Skip to content
Open
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
84 changes: 84 additions & 0 deletions DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,90 @@ 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<ExecutionSummaryPlugin>.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<ExecutionSummaryPluginReportByUrl>(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<string, int> { ["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_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<ExecutionSummaryPluginReportByUrl>(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<string, int> { ["GET https://api.contoso.com/users"] = 1 }, category.Value);
}

[Fact]
public async Task ExecutionSummary_ByMessageType_ExcludesInternalMessageTypes()
{
var plugin = CreateExecutionSummaryPlugin("messageType");
var args = Recording(PassedThroughRequestLogs());

await plugin.AfterRecordingStopAsync(args, CancellationToken.None);

var report = GetReport<ExecutionSummaryPluginReportByMessageType>(args, plugin.Name);
Assert.Equal(
["Requests intercepted", "Requests passed through"],
report.Data.Keys.Order());
Assert.Equal(
new Dictionary<string, int> { ["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()
{
Expand Down
47 changes: 14 additions & 33 deletions DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -115,31 +113,29 @@ private static Dictionary<string, Dictionary<string, Dictionary<string, int>>> 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);
Comment thread
waldekmastykarz marked this conversation as resolved.
Resolved
}

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))
Expand All @@ -163,13 +159,12 @@ private static Dictionary<string, Dictionary<string, Dictionary<string, int>>> 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 = [];
Expand Down Expand Up @@ -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"
};
}
42 changes: 23 additions & 19 deletions DevProxy.Plugins/Reporting/ExecutionSummaryPluginReportBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestLog> 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<string>()
.OrderBy(log => log)
.GroupBy(log => log)
.ToDictionary(group => group.Key, group => group.Count());
Expand Down
Loading