From 7c1f4bcf1bc3fc412dd61c50d01009a5842d083b Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 5 Nov 2026 11:42:55 +0100 Subject: [PATCH 1/2] Exclude internal message types from ExecutionSummaryPlugin report Timestamp, FinishedProcessingRequest and Skipped logs were falling into an "Unknown" category (with timestamps and URLs as keys). Map all message types explicitly in a single shared helper: exclude proxy-internal and diagnostic types, and map Normal/Processed to proper categories. Adds regression tests. Closes dotnet/dev-proxy#2078 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ReportingPluginsIntegrationTests.cs | 67 +++++++++++++++++++ .../Reporting/ExecutionSummaryPlugin.cs | 47 ++++--------- .../ExecutionSummaryPluginReportBase.cs | 42 ++++++------ 3 files changed, 104 insertions(+), 52 deletions(-) diff --git a/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs b/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs index 4d886c24c..f1d199aab 100644 --- a/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs +++ b/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs @@ -81,6 +81,73 @@ public async Task ExecutionSummary_GroupsInterceptedRequests() Assert.NotEmpty(report.Data); } + // Engine-shaped log stream for a single passed-through request, including the + // proxy-internal lifecycle messages (timestamp, intercepted response, finished) + // that must never surface as summary categories. + private static RequestLog[] PassedThroughRequestLogs() + { + var exchange = TestExchange.Request("GET", "https://api.contoso.com/users"); + var context = new LoggingContext(exchange.Session); + return + [ + exchange.AsRequestLog(), + new("2026-11-03T15:42:24.7350485+00:00", MessageType.Timestamp, context), + new("URL not matched", MessageType.Skipped, context), + new("Passed through", MessageType.PassedThrough, context), + new("GET https://api.contoso.com/users", MessageType.InterceptedResponse, context), + new("GET https://api.contoso.com/users", MessageType.FinishedProcessingRequest, context), + ]; + } + + private static ExecutionSummaryPlugin CreateExecutionSummaryPlugin(string groupBy) => + new( + TestDefaults.HttpClient, + NullLogger.Instance, + KestrelProxyHarness.BuildUrlsToWatch("api.contoso.com"), + new TestProxyConfiguration(), + PluginConfig.FromJson($$"""{ "groupBy": "{{groupBy}}" }""")); + + [Fact] + public async Task ExecutionSummary_ByUrl_ExcludesInternalMessageTypes() + { + var plugin = CreateExecutionSummaryPlugin("url"); + var args = Recording(PassedThroughRequestLogs()); + + await plugin.AfterRecordingStopAsync(args, CancellationToken.None); + + var report = GetReport(args, plugin.Name); + var request = Assert.Single(report.Data); + Assert.Equal("GET https://api.contoso.com/users", request.Key); + var category = Assert.Single(request.Value); + Assert.Equal("Requests passed through", category.Key); + Assert.Equal(new Dictionary { ["Passed through"] = 1 }, category.Value); + + var markdown = report.ToMarkdown(); + Assert.DoesNotContain("Unknown", markdown, StringComparison.Ordinal); + Assert.DoesNotContain("2026-11-03", markdown, StringComparison.Ordinal); + Assert.DoesNotContain("Unknown", report.ToPlainText(), StringComparison.Ordinal); + } + + [Fact] + public async Task ExecutionSummary_ByMessageType_ExcludesInternalMessageTypes() + { + var plugin = CreateExecutionSummaryPlugin("messageType"); + var args = Recording(PassedThroughRequestLogs()); + + await plugin.AfterRecordingStopAsync(args, CancellationToken.None); + + var report = GetReport(args, plugin.Name); + Assert.Equal( + ["Requests intercepted", "Requests passed through"], + report.Data.Keys.Order()); + Assert.Equal( + new Dictionary { ["GET https://api.contoso.com/users"] = 1 }, + report.Data["Requests passed through"]["Requests passed through"]); + + Assert.DoesNotContain("Unknown", report.ToMarkdown(), StringComparison.Ordinal); + Assert.DoesNotContain("Unknown", report.ToPlainText(), StringComparison.Ordinal); + } + [Fact] public async Task MinimalPermissions_ReportsPermissionsFromApiSpec() { diff --git a/DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs b/DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs index 90e092446..be62b477c 100644 --- a/DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs +++ b/DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs @@ -37,8 +37,6 @@ public sealed class ExecutionSummaryPlugin( pluginConfigurationSection) { private const string _groupByOptionName = "--summary-group-by"; - private const string _requestsInterceptedMessage = "Requests intercepted"; - private const string _requestsPassedThroughMessage = "Requests passed through"; public override string Name => nameof(ExecutionSummaryPlugin); @@ -115,31 +113,29 @@ private static Dictionary>> G foreach (var log in requestLogs) { - var message = GetRequestMessage(log); - if (log.MessageType == MessageType.InterceptedResponse) + var readableMessageType = ExecutionSummaryPluginReportBase.GetReadableMessageTypeForSummary(log.MessageType); + if (readableMessageType is null) { - // ignore intercepted response messages continue; } - if (log.MessageType == MessageType.InterceptedRequest) + var methodAndUrl = GetMethodAndUrl(log); + if (!data.TryGetValue(methodAndUrl, out var requestData)) { - var request = GetMethodAndUrl(log); - if (!data.ContainsKey(request)) - { - data.Add(request, []); - } + requestData = []; + data.Add(methodAndUrl, requestData); + } + if (log.MessageType == MessageType.InterceptedRequest) + { continue; } - // last line of the message is the method and URL of the request - var methodAndUrl = GetMethodAndUrl(log); - var readableMessageType = GetReadableMessageTypeForSummary(log.MessageType); - if (!data[methodAndUrl].TryGetValue(readableMessageType, out var value)) + var message = GetRequestMessage(log); + if (!requestData.TryGetValue(readableMessageType, out var value)) { value = []; - data[methodAndUrl].Add(readableMessageType, value); + requestData.Add(readableMessageType, value); } if (value.TryGetValue(message, out var val)) @@ -163,13 +159,12 @@ private static Dictionary>> G foreach (var log in requestLogs) { - if (log.MessageType == MessageType.InterceptedResponse) + var readableMessageType = ExecutionSummaryPluginReportBase.GetReadableMessageTypeForSummary(log.MessageType); + if (readableMessageType is null) { - // ignore intercepted response messages continue; } - var readableMessageType = GetReadableMessageTypeForSummary(log.MessageType); if (!data.TryGetValue(readableMessageType, out var value)) { value = []; @@ -234,18 +229,4 @@ private static string GetMethodAndUrl(RequestLog requestLog) ? $"{requestLog.Context.Session.Request.Method} {requestLog.Context.Session.Request.RequestUri}" : "Undefined"; } - -#pragma warning disable IDE0072 - private static string GetReadableMessageTypeForSummary(MessageType messageType) => messageType switch -#pragma warning restore IDE0072 - { - MessageType.Chaos => "Requests with chaos", - MessageType.Failed => "Failures", - MessageType.InterceptedRequest => _requestsInterceptedMessage, - MessageType.Mocked => "Requests mocked", - MessageType.PassedThrough => _requestsPassedThroughMessage, - MessageType.Tip => "Tips", - MessageType.Warning => "Warnings", - _ => "Unknown" - }; } diff --git a/DevProxy.Plugins/Reporting/ExecutionSummaryPluginReportBase.cs b/DevProxy.Plugins/Reporting/ExecutionSummaryPluginReportBase.cs index ad048c696..6fce6b6d3 100644 --- a/DevProxy.Plugins/Reporting/ExecutionSummaryPluginReportBase.cs +++ b/DevProxy.Plugins/Reporting/ExecutionSummaryPluginReportBase.cs @@ -21,30 +21,34 @@ public abstract class ExecutionSummaryPluginReportBase : IJsonReport public object ToJson() => Data; + // Returns null for message types that are excluded from the summary, + // like proxy-internal lifecycle messages (timestamps, finished processing) + // and diagnostic messages (intercepted responses, skipped plugins) + internal static string? GetReadableMessageTypeForSummary(MessageType messageType) => messageType switch + { + MessageType.Chaos => "Requests with chaos", + MessageType.Failed => "Failures", + MessageType.InterceptedRequest => RequestsInterceptedMessage, + MessageType.Mocked => "Requests mocked", + MessageType.Normal => "Information", + MessageType.PassedThrough => RequestsPassedThroughMessage, + MessageType.Processed => "Requests processed", + MessageType.Tip => "Tips", + MessageType.Warning => "Warnings", + MessageType.InterceptedResponse or + MessageType.FinishedProcessingRequest or + MessageType.Skipped or + MessageType.Timestamp => null, + _ => null + }; + protected static void AddExecutionSummaryReportSummary(IEnumerable requestLogs, StringBuilder sb) { ArgumentNullException.ThrowIfNull(sb); - static string getReadableMessageTypeForSummary(MessageType messageType) - { -#pragma warning disable IDE0072 - return messageType switch -#pragma warning restore IDE0072 - { - MessageType.Chaos => "Requests with chaos", - MessageType.Failed => "Failures", - MessageType.InterceptedRequest => RequestsInterceptedMessage, - MessageType.Mocked => "Requests mocked", - MessageType.PassedThrough => RequestsPassedThroughMessage, - MessageType.Tip => "Tips", - MessageType.Warning => "Warnings", - _ => "Unknown" - }; - } - var data = requestLogs - .Where(log => log.MessageType != MessageType.InterceptedResponse) - .Select(log => getReadableMessageTypeForSummary(log.MessageType)) + .Select(log => GetReadableMessageTypeForSummary(log.MessageType)) + .OfType() .OrderBy(log => log) .GroupBy(log => log) .ToDictionary(group => group.Key, group => group.Count()); From ae9dca1e619e0b96a023c3ea5250320c603ee666 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 5 Nov 2026 11:46:24 +0100 Subject: [PATCH 2/2] Add regression test for orphaned pass-through log in URL grouping Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ReportingPluginsIntegrationTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs b/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs index f1d199aab..aa02983a5 100644 --- a/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs +++ b/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs @@ -128,6 +128,23 @@ public async Task ExecutionSummary_ByUrl_ExcludesInternalMessageTypes() Assert.DoesNotContain("Unknown", report.ToPlainText(), StringComparison.Ordinal); } + [Fact] + public async Task ExecutionSummary_ByUrl_HandlesLogWithoutPrecedingInterceptedRequest() + { + var plugin = CreateExecutionSummaryPlugin("url"); + var exchange = TestExchange.Request("GET", "https://api.contoso.com/users"); + var args = Recording([exchange.AsRequestLog(MessageType.PassedThrough)]); + + await plugin.AfterRecordingStopAsync(args, CancellationToken.None); + + var report = GetReport(args, plugin.Name); + var request = Assert.Single(report.Data); + Assert.Equal("GET https://api.contoso.com/users", request.Key); + var category = Assert.Single(request.Value); + Assert.Equal("Requests passed through", category.Key); + Assert.Equal(new Dictionary { ["GET https://api.contoso.com/users"] = 1 }, category.Value); + } + [Fact] public async Task ExecutionSummary_ByMessageType_ExcludesInternalMessageTypes() {