Skip to content

Commit 534080d

Browse files
feat: merge main into release/2.0.0
2 parents bff47df + 40298cd commit 534080d

11 files changed

Lines changed: 509 additions & 48 deletions

File tree

DebugProbe.AspNetCore.Tests/Configuration/DebugProbeOptionsTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,25 @@ public void MaxBodyCaptureSizeKb_zero_is_valid()
130130

131131
Assert.Equal(0, options.MaxBodyCaptureSizeKb);
132132
}
133+
134+
[Fact]
135+
public void SlowRequestThresholdMs_defaults_to_1000()
136+
{
137+
var options = new DebugProbeOptions();
138+
Assert.Equal(1000, options.SlowRequestThresholdMs);
139+
}
140+
141+
[Fact]
142+
public void SlowRequestThresholdMs_can_be_configured()
143+
{
144+
var services = new ServiceCollection();
145+
services.AddDebugProbe(options =>
146+
{
147+
options.SlowRequestThresholdMs = 500;
148+
});
149+
150+
using var provider = services.BuildServiceProvider();
151+
var options = provider.GetRequiredService<DebugProbeOptions>();
152+
Assert.Equal(500, options.SlowRequestThresholdMs);
153+
}
133154
}

DebugProbe.AspNetCore.Tests/Rendering/HtmlRendererTests.cs

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using DebugProbe.AspNetCore.Internal.Rendering;
22
using DebugProbe.AspNetCore.Internal.Resources;
33
using DebugProbe.AspNetCore.Models;
4+
using DebugProbe.AspNetCore.Options;
45

56
namespace DebugProbe.AspNetCore.Tests.Rendering;
67

@@ -242,6 +243,111 @@ public void Details_page_renders_csharp_copy_attributes_and_buttons()
242243
Assert.Equal(2, occurrences);
243244
}
244245

246+
[Fact]
247+
public void Details_page_renders_markdown_copy_attributes_and_buttons()
248+
{
249+
var entry = CreateEntry();
250+
entry.OutgoingRequests.Add(new DebugOutgoingRequest
251+
{
252+
Method = "PUT",
253+
Url = "https://external-api.test/v1/update",
254+
StatusCode = 200,
255+
DurationMs = 120,
256+
RequestBody = "{\"name\":\"John\"}",
257+
RequestHeaders = new Dictionary<string, string> { ["Authorization"] = "Bearer token" }
258+
});
259+
260+
var html = HtmlRenderer.RenderDetailsPage(
261+
entry,
262+
CreateEnvironment(),
263+
"{\"request\":true}",
264+
"{\"response\":true}");
265+
266+
// 1. Verify Incoming Request Card attributes and button
267+
Assert.Contains("data-method=\"POST\"", html);
268+
Assert.Contains("data-url=\"http://example.test/orders?id=10\"", html);
269+
Assert.Contains("data-headers=\"{&quot;X-Test&quot;:&quot;yes&quot;}\"", html);
270+
Assert.Contains("data-body=\"{&quot;request&quot;:true}\"", html);
271+
Assert.Contains("class=\"markdown-copy-btn\"", html);
272+
273+
// 2. Verify Outgoing Request Card attributes and button
274+
Assert.Contains("data-method=\"PUT\"", html);
275+
Assert.Contains("data-url=\"https://external-api.test/v1/update\"", html);
276+
Assert.Contains("data-headers=\"{&quot;Authorization&quot;:&quot;Bearer token&quot;}\"", html);
277+
Assert.Contains("data-body=\"{&quot;name&quot;:&quot;John&quot;}\"", html);
278+
279+
// 3. Verify Response Card has no markdown copy button (only 2 copy markdown buttons in total should exist in HTML markup)
280+
var occurrences = (html.Length - html.Replace("class=\"markdown-copy-btn\"", "").Length) / "class=\"markdown-copy-btn\"".Length;
281+
Assert.Equal(2, occurrences);
282+
}
283+
284+
[Fact]
285+
public void Render_index_page_with_slow_badge()
286+
{
287+
var items = new List<DebugEntry>
288+
{
289+
new DebugEntry { Id = "1", Method = "GET", Path = "/1", DurationMs = 500, StatusCode = 200 },
290+
new DebugEntry { Id = "2", Method = "GET", Path = "/2", DurationMs = 1500, StatusCode = 200 }
291+
};
292+
293+
// Case 1: Threshold = 1000 (Default)
294+
var htmlDefault = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
295+
Assert.Contains("/debug/1", htmlDefault);
296+
Assert.Contains("/debug/2", htmlDefault);
297+
298+
Assert.Contains("500 ms</td>", htmlDefault);
299+
Assert.Contains("1500 ms <span class=\"dbp-badge dbp-badge-slow\" title=\"Exceeds 1000ms threshold\">SLOW</span>", htmlDefault);
300+
301+
// Case 2: Threshold = 0 (disabled)
302+
var htmlDisabled = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = 0 });
303+
Assert.DoesNotContain("<span class=\"dbp-badge dbp-badge-slow\"", htmlDisabled);
304+
305+
// Case 3: Threshold = 2000
306+
var htmlHigh = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = 2000 });
307+
Assert.DoesNotContain("<span class=\"dbp-badge dbp-badge-slow\"", htmlHigh);
308+
}
309+
310+
[Fact]
311+
public void Render_details_page_with_slow_badge()
312+
{
313+
var entry = CreateEntry();
314+
entry.DurationMs = 1200; // > 1000
315+
entry.OutgoingRequests.Add(new DebugOutgoingRequest
316+
{
317+
Method = "GET",
318+
Url = "https://external-api.test",
319+
StatusCode = 200,
320+
DurationMs = 1500, // > 1000
321+
TimestampUtc = entry.Timestamp.UtcDateTime.AddMilliseconds(200),
322+
IsSuccessStatusCode = true
323+
});
324+
entry.OutgoingRequests.Add(new DebugOutgoingRequest
325+
{
326+
Method = "GET",
327+
Url = "https://external-api2.test",
328+
StatusCode = 200,
329+
DurationMs = 200, // < 1000
330+
TimestampUtc = entry.Timestamp.UtcDateTime.AddMilliseconds(400),
331+
IsSuccessStatusCode = true
332+
});
333+
334+
// Case 1: Threshold = 1000
335+
var html = HtmlRenderer.RenderDetailsPage(entry, CreateEnvironment(), "{}", "{}", new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
336+
337+
// Detail Header should have badge
338+
Assert.Contains("1200 ms <span class=\"dbp-badge dbp-badge-slow\" title=\"Exceeds 1000ms threshold\">SLOW</span>", html);
339+
340+
// Outgoing Request 1 (1500ms) should have badge in waterfall
341+
Assert.Contains("1500 ms <span class=\"dbp-badge dbp-badge-slow\" title=\"Exceeds 1000ms threshold\">SLOW</span>", html);
342+
343+
// Outgoing Request 2 (200ms) should NOT have badge in waterfall
344+
Assert.Contains("200 ms</div>", html);
345+
346+
// Case 2: Threshold = 0
347+
var htmlDisabled = HtmlRenderer.RenderDetailsPage(entry, CreateEnvironment(), "{}", "{}", new DebugProbeOptions { SlowRequestThresholdMs = 0 });
348+
Assert.DoesNotContain("<span class=\"dbp-badge dbp-badge-slow\"", htmlDisabled);
349+
}
350+
245351
private static DebugEntry CreateEntry()
246352
{
247353
return new DebugEntry
@@ -284,4 +390,137 @@ private static DebugEnvironment CreateEnvironment()
284390
AssemblyVersion = "1.0.0"
285391
};
286392
}
393+
394+
[Fact]
395+
public void Render_slow_badge_boundary_exact_threshold()
396+
{
397+
var items = new List<DebugEntry>
398+
{
399+
new DebugEntry { Id = "1", Method = "GET", Path = "/exact", DurationMs = 1000, StatusCode = 200 }
400+
};
401+
var html = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
402+
Assert.Contains("1000 ms <span class=\"dbp-badge dbp-badge-slow\" title=\"Exceeds 1000ms threshold\">SLOW</span>", html);
403+
}
404+
405+
[Fact]
406+
public void Render_slow_badge_boundary_threshold_minus_one()
407+
{
408+
var items = new List<DebugEntry>
409+
{
410+
new DebugEntry { Id = "1", Method = "GET", Path = "/minus-one", DurationMs = 999, StatusCode = 200 }
411+
};
412+
var html = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
413+
Assert.Contains("999 ms</td>", html);
414+
Assert.DoesNotContain("<span class=\"dbp-badge dbp-badge-slow\"", html);
415+
}
416+
417+
[Fact]
418+
public void Render_slow_badge_boundary_threshold_plus_one()
419+
{
420+
var items = new List<DebugEntry>
421+
{
422+
new DebugEntry { Id = "1", Method = "GET", Path = "/plus-one", DurationMs = 1001, StatusCode = 200 }
423+
};
424+
var html = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
425+
Assert.Contains("1001 ms <span class=\"dbp-badge dbp-badge-slow\" title=\"Exceeds 1000ms threshold\">SLOW</span>", html);
426+
}
427+
428+
[Fact]
429+
public void Render_slow_badge_boundary_zero_threshold_disabled()
430+
{
431+
var items = new List<DebugEntry>
432+
{
433+
new DebugEntry { Id = "1", Method = "GET", Path = "/zero", DurationMs = 5000, StatusCode = 200 }
434+
};
435+
var html = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = 0 });
436+
Assert.Contains("5000 ms</td>", html);
437+
Assert.DoesNotContain("<span class=\"dbp-badge dbp-badge-slow\"", html);
438+
}
439+
440+
[Fact]
441+
public void Render_slow_badge_boundary_negative_threshold_disabled()
442+
{
443+
var items = new List<DebugEntry>
444+
{
445+
new DebugEntry { Id = "1", Method = "GET", Path = "/negative", DurationMs = 5000, StatusCode = 200 }
446+
};
447+
var html = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = -100 });
448+
Assert.Contains("5000 ms</td>", html);
449+
Assert.DoesNotContain("<span class=\"dbp-badge dbp-badge-slow\"", html);
450+
}
451+
452+
[Fact]
453+
public void Render_slow_badge_large_duration_formatting()
454+
{
455+
var items = new List<DebugEntry>
456+
{
457+
new DebugEntry { Id = "1", Method = "GET", Path = "/large", DurationMs = 120500, StatusCode = 200 }
458+
};
459+
var html = HtmlRenderer.RenderIndexPage(items, new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
460+
Assert.Contains("120500 ms <span class=\"dbp-badge dbp-badge-slow\" title=\"Exceeds 1000ms threshold\">SLOW</span>", html);
461+
}
462+
463+
[Fact]
464+
public void Render_details_page_independent_outgoing_call_badge_behavior()
465+
{
466+
var entry = CreateEntry();
467+
entry.DurationMs = 500; // < 1000 (not slow)
468+
entry.OutgoingRequests.Add(new DebugOutgoingRequest
469+
{
470+
Method = "GET",
471+
Url = "https://external-api.test",
472+
StatusCode = 200,
473+
DurationMs = 1500, // > 1000 (slow)
474+
TimestampUtc = entry.Timestamp.UtcDateTime.AddMilliseconds(100),
475+
IsSuccessStatusCode = true
476+
});
477+
478+
var html = HtmlRenderer.RenderDetailsPage(entry, CreateEnvironment(), "{}", "{}", new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
479+
480+
// Root request should not have the slow badge
481+
Assert.Contains("<strong>500 ms</strong>", html);
482+
Assert.DoesNotContain(" 500 ms <span class=\"dbp-badge dbp-badge-slow\"", html);
483+
484+
// Outgoing request should independently have the slow badge
485+
Assert.Contains("1500 ms <span class=\"dbp-badge dbp-badge-slow\" title=\"Exceeds 1000ms threshold\">SLOW</span>", html);
486+
}
487+
488+
[Fact]
489+
public void Render_details_page_outgoing_call_boundary_exact()
490+
{
491+
var entry = CreateEntry();
492+
entry.DurationMs = 200;
493+
entry.OutgoingRequests.Add(new DebugOutgoingRequest
494+
{
495+
Method = "GET",
496+
Url = "https://external-api.test",
497+
StatusCode = 200,
498+
DurationMs = 1000, // exact
499+
TimestampUtc = entry.Timestamp.UtcDateTime.AddMilliseconds(100),
500+
IsSuccessStatusCode = true
501+
});
502+
503+
var html = HtmlRenderer.RenderDetailsPage(entry, CreateEnvironment(), "{}", "{}", new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
504+
Assert.Contains("1000 ms <span class=\"dbp-badge dbp-badge-slow\" title=\"Exceeds 1000ms threshold\">SLOW</span>", html);
505+
}
506+
507+
[Fact]
508+
public void Render_details_page_outgoing_call_boundary_minus_one()
509+
{
510+
var entry = CreateEntry();
511+
entry.DurationMs = 200;
512+
entry.OutgoingRequests.Add(new DebugOutgoingRequest
513+
{
514+
Method = "GET",
515+
Url = "https://external-api.test",
516+
StatusCode = 200,
517+
DurationMs = 999, // below threshold
518+
TimestampUtc = entry.Timestamp.UtcDateTime.AddMilliseconds(100),
519+
IsSuccessStatusCode = true
520+
});
521+
522+
var html = HtmlRenderer.RenderDetailsPage(entry, CreateEnvironment(), "{}", "{}", new DebugProbeOptions { SlowRequestThresholdMs = 1000 });
523+
Assert.Contains("999 ms</div>", html);
524+
Assert.DoesNotContain(" 999 ms <span class=\"dbp-badge dbp-badge-slow\"", html);
525+
}
287526
}

DebugProbe.AspNetCore/Assets/css/debugprobe.css

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,25 @@ pre {
983983
font-size: 12px;
984984
}
985985

986+
.dbp-badge {
987+
display: inline-flex;
988+
align-items: center;
989+
justify-content: center;
990+
padding: 2px 6px;
991+
border-radius: 4px;
992+
font-size: 11px;
993+
font-weight: 700;
994+
line-height: 1;
995+
margin-left: 6px;
996+
vertical-align: middle;
997+
}
998+
999+
.dbp-badge-slow {
1000+
background: rgba(255, 200, 0, 0.15);
1001+
color: #b45309;
1002+
border: 1px solid rgba(255, 200, 0, 0.3);
1003+
}
1004+
9861005
/* =========================
9871006
Diff
9881007
========================= */
@@ -1294,7 +1313,8 @@ pre {
12941313
cURL Copy Button & Tooltip
12951314
========================= */
12961315
.curl-copy-btn,
1297-
.csharp-copy-btn {
1316+
.csharp-copy-btn,
1317+
.markdown-copy-btn {
12981318
display: inline-flex;
12991319
align-items: center;
13001320
justify-content: center;
@@ -1310,14 +1330,16 @@ pre {
13101330
}
13111331

13121332
.curl-copy-btn:hover,
1313-
.csharp-copy-btn:hover {
1333+
.csharp-copy-btn:hover,
1334+
.markdown-copy-btn:hover {
13141335
background: #f9fafb;
13151336
border-color: #d1d5db;
13161337
color: #111827;
13171338
}
13181339

13191340
.curl-copy-btn svg,
1320-
.csharp-copy-btn svg {
1341+
.csharp-copy-btn svg,
1342+
.markdown-copy-btn svg {
13211343
width: 14px;
13221344
height: 14px;
13231345
stroke: currentColor;

DebugProbe.AspNetCore/Assets/html/_shared/layout.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<head>
1+
<head>
2+
<meta charset="utf-8">
23
<link rel="icon" href="/debug/favicon.ico" type="image/x-icon">
34
{{styles}}
45
</head>

DebugProbe.AspNetCore/Assets/html/details.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="container">
1+
<div class="container">
22

33
<a href="/debug">&#8592; Back</a>
44

@@ -35,7 +35,7 @@
3535

3636
<div class="details-item">
3737
<span>Duration</span>
38-
<strong>{{durationMs}} ms</strong>
38+
<strong>{{durationMs}} ms{{durationBadge}}</strong>
3939
</div>
4040

4141
<div class="details-item">

0 commit comments

Comments
 (0)