diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4235d02..c860b70 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup .NET uses: actions/setup-dotnet@v5 diff --git a/DebugProbe.AspNetCore/Assets/css/debugprobe.css b/DebugProbe.AspNetCore/Assets/css/debugprobe.css index 43d30ba..63d4b5c 100644 --- a/DebugProbe.AspNetCore/Assets/css/debugprobe.css +++ b/DebugProbe.AspNetCore/Assets/css/debugprobe.css @@ -1,4 +1,4 @@ -/* ========================= +/* ========================= Base ========================= */ @@ -1128,3 +1128,86 @@ pre { background: #4a1717; color: #ff8a8a !important; } + +/* ========================= + Waterfall View +========================= */ + +.waterfall-container { + margin-bottom: 12px; +} + +.waterfall-container .trace-card-main { + padding: 12px 14px; +} + +.waterfall-container .trace-card-header { + padding: 0 0 12px 0; +} + +.waterfall-container .trace-dot { + background: #9b51e0; +} + +.waterfall-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.waterfall-row:last-child { + margin-bottom: 0; +} + +.wf-label { + flex: 0 0 200px; + width: 200px; + min-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; + font-size: 12px; + color: #4b5563; +} + +.wf-track { + flex: 1; + position: relative; + height: 20px; + background: #f3f4f6; + border-radius: 4px; +} + +.wf-bar { + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background: #9b51e0; + color: #fff; + font-size: 10px; + font-weight: bold; + border-radius: 3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 4px; + box-sizing: border-box; +} + +.wf-bar--error { + background: #e74c3c; +} + +@media (max-width: 640px) { + .wf-label { + flex: 0 0 100px; + width: 100px; + min-width: 100px; + } +} + diff --git a/DebugProbe.AspNetCore/DebugProbe.AspNetCore.csproj b/DebugProbe.AspNetCore/DebugProbe.AspNetCore.csproj index 0e0ab27..22f74ae 100644 --- a/DebugProbe.AspNetCore/DebugProbe.AspNetCore.csproj +++ b/DebugProbe.AspNetCore/DebugProbe.AspNetCore.csproj @@ -8,7 +8,7 @@ 1591 DebugProbe.AspNetCore - 1.6.5 + 1.6.6 Georgi Hristov @@ -17,7 +17,14 @@ aspnetcore;debugging;http;middleware;diagnostics;tracing;observability;api-debugging;developer-tools;trace-comparison;environment-comparison - Fixes response size tracking to report the actual number of bytes sent to the client instead of the captured body size. Adds validation for MaxBodyCaptureSizeKb to reject negative values while allowing 0 to disable body capture. Updates Microsoft.AspNetCore.TestHost to 8.0.28 and xunit.runner.visualstudio to 3.1.5. + ## What's New + + - Added configurable RoutePrefix support for customizing DebugProbe endpoints. + - Fixed endpoint mapping to respect production UI configuration. + - Fixed outgoing HTTP body capture stream preservation. + - Fixed outgoing HTTP body capture to avoid reading beyond the configured limit. + + Thanks to @DevSars24 for the community contributions. icon.png diff --git a/DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs b/DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs index 49aeb64..38308e9 100644 --- a/DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs +++ b/DebugProbe.AspNetCore/Internal/Rendering/HtmlRenderer.cs @@ -99,8 +99,12 @@ public static string RenderDetailsPage(DebugEntry x, DebugEnvironment e, string BuildPayloadSection("Body", res, "body") ]); + var waterfall = BuildWaterfallSection(x); + var outgoingRequests = string.Join("", x.OutgoingRequests.Select(BuildOutgoingRequestCard)); + var combinedOutgoing = waterfall + outgoingRequests; + var content = EmbeddedResources.Details .Replace("{{method}}", Encode(x.Method)) .Replace("{{path}}", Encode(pathWithQuery)) @@ -124,9 +128,9 @@ public static string RenderDetailsPage(DebugEntry x, DebugEnvironment e, string .Replace("{{dateFormat}}", e.DateFormat ?? "") .Replace("{{assemblyVersion}}", Encode(e.AssemblyVersion)) .Replace("{{outgoingRequests}}", - string.IsNullOrWhiteSpace(outgoingRequests) + string.IsNullOrWhiteSpace(combinedOutgoing) ? "
No outgoing dependency calls
" - : outgoingRequests) + : combinedOutgoing) .Replace("{{incomingRequest}}", incomingRequest) .Replace("{{incomingResponse}}", incomingResponse); @@ -188,6 +192,67 @@ private static string BuildOutgoingRequestCard(DebugOutgoingRequest request) details: details); } + private static string BuildWaterfallSection(DebugEntry entry) + { + const double MinPercent = 0.0; + const double MaxPercent = 100.0; + + if (entry.OutgoingRequests.Count == 0) + { + return string.Empty; + } + + var totalSpan = (double)entry.DurationMs; + if (totalSpan <= 0) + { + totalSpan = 1.0; + } + + var rowsHtml = new List(); + + foreach (var outgoing in entry.OutgoingRequests) + { + var startOffsetMs = (outgoing.TimestampUtc - entry.Timestamp.UtcDateTime).TotalMilliseconds - outgoing.DurationMs; + + var left = Math.Clamp((startOffsetMs / totalSpan) * MaxPercent, MinPercent, MaxPercent); + var width = Math.Clamp(((double)outgoing.DurationMs / totalSpan) * MaxPercent, MinPercent, MaxPercent); + + var leftStr = left.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); + var widthStr = width.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); + + var barClass = "wf-bar"; + if (!outgoing.IsSuccessStatusCode || !string.IsNullOrWhiteSpace(outgoing.Exception)) + { + barClass += " wf-bar--error"; + } + + var displayLabel = GetDisplayTarget(outgoing.Url); + + rowsHtml.Add($@" +
+ {Encode(displayLabel)} +
+
{outgoing.DurationMs} ms
+
+
"); + } + + return $@" +
+
+
+
+ + Waterfall Timeline +
+
+
+ {string.Join("", rowsHtml)} +
+
+
"; + } + private static string BuildTraceCard(string label, string method, string target, string classes, IEnumerable details, int? statusCode = null, string? statusText = null, long? durationMs = null) { var targetHost = GetDisplayTarget(target);