Skip to content

Commit 37c1df1

Browse files
committed
Add error group counts to crash reporting page
1 parent 707a1a4 commit 37c1df1

File tree

10 files changed

+100
-10
lines changed

10 files changed

+100
-10
lines changed

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@
1111
<PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" />
1212
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
1313
<PackageVersion Include="Serilog.Sinks.Raygun" Version="8.2.0" />
14+
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
15+
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />
1416
</ItemGroup>
1517
</Project>

src/Minigun/Areas/CrashReporting/Controllers/CrashReportingController.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,16 @@ [FromQuery] DateTime endTime
8686

8787
return PartialView("_ErrorTimeseries", errorTimeseries);
8888
}
89+
90+
[HttpGet("/crashreporting/error-group-count")]
91+
public async Task<IActionResult> ErrorGroupCount(
92+
[FromQuery] string applicationIdentifier,
93+
[FromQuery] string errorGroupId,
94+
[FromQuery] DateTime startTime,
95+
[FromQuery] DateTime endTime
96+
)
97+
{
98+
var count = await _raygunApiService.GetErrorGroupCountAsync(applicationIdentifier, errorGroupId, startTime, endTime);
99+
return Content(count.ToString("N0"));
100+
}
89101
}

src/Minigun/Areas/CrashReporting/Views/CrashReporting/_ErrorGroups.cshtml

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
<label for="checkbox-all" class="sr-only">checkbox</label>
2929
</div>
3030
</th>
31-
<th scope="col" class="px-4 py-2 w-2/3">Message</th>
31+
<th scope="col" class="px-4 py-2 w-2/5">Message</th>
3232
<th scope="col" class="px-4 py-2 w-1/6">Last seen</th>
33-
<th scope="col" class="px-4 py-2 w-1/6">Status</th>
33+
<th scope="col" class="px-4 py-2 w-1/6">First seen</th>
34+
<th scope="col" class="px-4 py-2 w-1/6">Count</th>
3435
</tr>
3536
</thead>
3637
</table>
@@ -65,9 +66,10 @@
6566
<label for="checkbox-all" class="sr-only">checkbox</label>
6667
</div>
6768
</th>
68-
<th scope="col" class="px-4 py-2 w-2/3">Message</th>
69+
<th scope="col" class="px-4 py-2 w-2/5">Message</th>
6970
<th scope="col" class="px-4 py-2 w-1/6">Last seen</th>
70-
<th scope="col" class="px-4 py-2 w-1/6">Status</th>
71+
<th scope="col" class="px-4 py-2 w-1/6">First seen</th>
72+
<th scope="col" class="px-4 py-2 w-1/6">Count</th>
7173
</tr>
7274
</thead>
7375
<tbody>
@@ -83,14 +85,25 @@
8385
<td class="px-4 py-2 font-medium text-gray-900 truncate">
8486
<a href="@errorGroup.ApplicationUrl" class="font-medium text-blue-600 hover:underline">@errorGroup.Message</a>
8587
</td>
86-
<td class="px-4 py-2 whitespace-nowrap text-red-600">
88+
<td class="px-4 py-2 whitespace-nowrap text-gray-600">
8789
@{
8890
var lastSeenText = errorGroup.LastOccurredAt.ToHumanReadableTimeAgo();
8991
}
9092
@lastSeenText
9193
</td>
92-
<td class="px-4 py-2 whitespace-nowrap">
93-
@(errorGroup.ResolvedIn != null ? "Resolved" : "Active")
94+
<td class="px-4 py-2 whitespace-nowrap text-gray-600">
95+
@{
96+
var firstSeenText = errorGroup.CreatedAt.ToHumanReadableTimeAgo();
97+
}
98+
@firstSeenText
99+
</td>
100+
<td class="px-4 py-2 whitespace-nowrap text-gray-700 font-semibold"
101+
hx-get="/crashreporting/error-group-count"
102+
hx-vals='{"applicationIdentifier": "@Context.Request.Query["applicationIdentifier"]", "errorGroupId": "@errorGroup.Identifier", "startTime": "@Context.Request.Query["startTime"]", "endTime": "@Context.Request.Query["endTime"]"}'
103+
hx-trigger="intersect once"
104+
hx-target="#count-@errorGroup.Identifier"
105+
hx-indicator=".no-global-indicator">
106+
<span id="count-@errorGroup.Identifier" class="text-gray-400 animate-pulse">–</span>
94107
</td>
95108
</tr>
96109
}

src/Minigun/Minigun.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<PackageReference Include="Serilog.AspNetCore" />
1616
<PackageReference Include="Serilog.Enrichers.Environment" />
1717
<PackageReference Include="Serilog.Enrichers.Thread" />
18+
<PackageReference Include="Polly.Extensions.Http" />
19+
<PackageReference Include="Microsoft.Extensions.Http.Polly" />
1820
</ItemGroup>
1921

2022
<ItemGroup>

src/Minigun/Models/ErrorGroup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class ErrorGroup
1111
public ErrorGroupResolvedIn? ResolvedIn { get; set; }
1212
public bool DiscardNewOccurrences { get; set; }
1313
public string? ApplicationUrl { get; set; }
14+
public int Count { get; set; }
1415
}
1516

1617
public class ErrorGroupResolvedIn

src/Minigun/Program.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Mindscape.Raygun4Net.AspNetCore;
33
using Minigun.Middleware;
44
using Minigun.Services;
5+
using System.Threading.RateLimiting;
56
using Serilog;
67

78
var builder = WebApplication.CreateBuilder(args);
@@ -15,7 +16,11 @@
1516
builder.Services.AddRaygun(builder.Configuration).AddRaygunUserProvider();
1617

1718
builder.Services.AddHttpContextAccessor();
18-
builder.Services.AddHttpClient<IRaygunApiService, RaygunApiService>();
19+
20+
// Configure rate limiting for Raygun API (10 requests per second)
21+
builder.Services.AddTransient<RateLimitingHandler>();
22+
builder.Services.AddHttpClient<IRaygunApiService, RaygunApiService>()
23+
.AddHttpMessageHandler<RateLimitingHandler>();
1924

2025
builder.Services.Configure<RouteOptions>(options =>
2126
{

src/Minigun/Services/IRaygunApiService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ Task<List<TimeseriesData>> GetErrorTimeseriesAsync(string applicationId, DateTim
1414

1515
Task<List<HistogramData>> GetRumHistogramAsync(string applicationId, DateTime start, DateTime end, string[] metrics,
1616
string? filter = null);
17+
18+
Task<int> GetErrorGroupCountAsync(string applicationId, string errorGroupId, DateTime start, DateTime end);
1719
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Threading.RateLimiting;
2+
3+
namespace Minigun.Services;
4+
5+
public class RateLimitingHandler : DelegatingHandler
6+
{
7+
private readonly RateLimiter _rateLimiter = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
8+
{
9+
PermitLimit = 10,
10+
Window = TimeSpan.FromSeconds(1),
11+
SegmentsPerWindow = 1
12+
});
13+
14+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
15+
{
16+
using var lease = await _rateLimiter.AcquireAsync(1, cancellationToken);
17+
18+
if (!lease.IsAcquired)
19+
{
20+
throw new HttpRequestException("Rate limit exceeded");
21+
}
22+
23+
return await base.SendAsync(request, cancellationToken);
24+
}
25+
26+
protected override void Dispose(bool disposing)
27+
{
28+
if (disposing)
29+
{
30+
_rateLimiter.Dispose();
31+
}
32+
base.Dispose(disposing);
33+
}
34+
}

src/Minigun/Services/RaygunApiService.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public async Task<List<TimeseriesData>> GetErrorTimeseriesAsync(
9494
aggregation = "count",
9595
metrics = new[] { "errorInstances" },
9696
filter = errorGroupIds?.Any() == true
97-
? $"errorGroupIdentifier IN ({string.Join(", ", errorGroupIds.Select(id => $"'{id}'"))})"
97+
? $"errorGroupIdentifier IN ({string.Join(", ", errorGroupIds)})"
9898
: null
9999
};
100100

@@ -138,4 +138,23 @@ public async Task<List<HistogramData>> GetRumHistogramAsync(
138138
var responseContent = await response.Content.ReadAsStringAsync();
139139
return JsonSerializer.Deserialize<List<HistogramData>>(responseContent, JsonOptions) ?? [];
140140
}
141+
142+
public async Task<int> GetErrorGroupCountAsync(string applicationId, string errorGroupId, DateTime start, DateTime end)
143+
{
144+
try
145+
{
146+
var timeseriesData = await GetErrorTimeseriesAsync(applicationId, start, end, new List<string> { errorGroupId });
147+
148+
var totalCount = timeseriesData
149+
.SelectMany(ts => ts.Series ?? new List<TimeseriesPoint>())
150+
.Sum(point => (int)point.Value);
151+
152+
return totalCount;
153+
}
154+
catch (Exception)
155+
{
156+
// If we can't get data for this error group, set count to 0
157+
return 0;
158+
}
159+
}
141160
}

0 commit comments

Comments
 (0)