CherryPeak.CherryBoard.Client
1.5.0
dotnet add package CherryPeak.CherryBoard.Client --version 1.5.0
NuGet\Install-Package CherryPeak.CherryBoard.Client -Version 1.5.0
<PackageReference Include="CherryPeak.CherryBoard.Client" Version="1.5.0" />
<PackageVersion Include="CherryPeak.CherryBoard.Client" Version="1.5.0" />
<PackageReference Include="CherryPeak.CherryBoard.Client" />
paket add CherryPeak.CherryBoard.Client --version 1.5.0
#r "nuget: CherryPeak.CherryBoard.Client, 1.5.0"
#:package CherryPeak.CherryBoard.Client@1.5.0
#addin nuget:?package=CherryPeak.CherryBoard.Client&version=1.5.0
#tool nuget:?package=CherryPeak.CherryBoard.Client&version=1.5.0
CherryBoard Client
A lightweight, high-performance .NET client library for capturing and sending errors to CherryBoard Dashboard. Similar to Sentry, but tailored for your infrastructure.
Features
- 🚀 Automatic Exception Capture - Middleware automatically captures unhandled exceptions
- 📡 Reliable Transport (v1.3.0+) - A background worker delivers errors continuously (near real-time), batched; buffered errors are flushed on shutdown and global unhandled/unobserved exceptions are captured
- 🔄 Retry Logic - Exponential backoff retry mechanism for failed requests
- 💾 Offline Queue - Local queuing support for offline scenarios
- 🎯 Sampling - Configure sample rates to control error volume
- 🔍 Rich Context - Automatically captures HTTP request details, user info, exception type/inner exception, and metadata
- ⚡ Non-Blocking - Capturing enqueues and returns immediately; never blocks your app (drops, rather than blocks, when the buffer is full)
- 📝 Log Integration (v1.1.0+) - Forward
Warning/Error/Criticallogs to CherryBoard, including background jobs and hosted services the middleware can't see - ⏱️ Request Timing (v1.4.0+) - Optional per-route latency tracking, aggregated in-process and shown next to the browser SDK's numbers for the same route
Installation
dotnet add package CherryPeak.CherryBoard.Client
Quick Start
1. Configure in Program.cs
using CherryPeak.CherryBoard.Client.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add CherryBoard
builder.Services.AddCherryBoard(options =>
{
options.ApiKey = "your-api-key-here";
options.ApiUrl = "https://your-cherryboard-host"; // your CherryBoard instance URL
options.Environment = builder.Environment.EnvironmentName;
options.EnableAutomaticCapture = true;
options.MaxBatchSize = 50; // max errors per delivery request
options.MaxQueueItems = 1000; // (v1.3.0+) in-memory buffer cap; drops when full
options.SampleRate = 1.0; // Capture 100% of errors
// For development with self-signed certificates (localhost)
if (builder.Environment.IsDevelopment())
{
options.DisableSslValidation = true;
}
});
// (v1.1.0+) Forward Warning/Error/Critical logs to CherryBoard — including background
// jobs, hosted services, and caught-and-logged errors the middleware never sees.
builder.Logging.AddCherryBoardLogging();
var app = builder.Build();
// Register middleware (if EnableAutomaticCapture is true, it will capture unhandled exceptions)
app.UseCherryBoard();
app.Run();
2. Configuration Options
| Option | Description | Valid Range | Default |
|---|---|---|---|
ApiKey |
Your CherryPeak API key | Required, non-empty | N/A |
ApiUrl |
CherryBoard Dashboard API URL | Required, non-empty | N/A |
Environment |
Environment name (Production, Staging, etc.) | Any string | "Production" |
EnableAutomaticCapture |
Enable automatic capture middleware for unhandled exceptions | true/false | true |
MaxBatchSize |
Max errors sent in a single delivery request | 1-1000 | 50 |
MaxQueueItems (v1.3.0+) |
In-memory buffer cap; new errors are dropped (never block) when full | 1-1,000,000 | 1000 |
ShutdownTimeoutSeconds (v1.3.0+) |
Max seconds to flush buffered errors on shutdown | 0-60 | 2 |
MaxRetries |
Number of retry attempts for failed requests | 0-10 | 3 |
EnableOfflineQueue |
Enable local queuing for offline support | true/false | true |
SampleRate |
Percentage of errors to capture (0.0-1.0) | 0.0-1.0 | 1.0 |
DisableSslValidation |
Disable SSL validation for self-signed certs (dev only) | true/false | false |
MinimumLogLevel (v1.1.0+) |
Lowest log level forwarded by AddCherryBoardLogging() |
Trace-None |
Warning |
EnablePerformanceTracking (v1.4.0+) |
Time requests and report per-route rollups. Also needs UseCherryBoardPerformance() |
true/false | false |
MetricsFlushIntervalSeconds (v1.4.0+) |
How often rollups are posted | 10-3600 | 60 |
MetricsMaxRoutes (v1.4.0+) |
Distinct routes tracked per process | 1-2000 | 200 |
BatchIntervalSeconds |
Deprecated (v1.3.0) — delivery is now continuous, so this is no longer used | 1-3600 | 30 |
Note: Configuration is validated during AddCherryBoard() registration. Invalid values will throw ArgumentException with clear error messages.
3. Manual Error Capture
You can also manually capture exceptions:
public class MyService
{
private readonly ICherryBoardClient _client;
public MyService(ICherryBoardClient client)
{
_client = client;
}
public async Task DoSomethingAsync()
{
try
{
// Your code
}
catch (Exception ex)
{
// Manually capture exception with custom metadata
await _client.CaptureExceptionAsync(ex, errorData =>
{
errorData.UserId = "user-123";
errorData.Metadata = System.Text.Json.JsonSerializer.Serialize(new
{
CustomField = "CustomValue",
Operation = "DoSomething"
});
});
throw; // Re-throw if needed
}
}
}
4. Manual Error Data
Send custom error data without an exception. CaptureExceptionAsync fills ExceptionType,
StackTrace, and InnerException automatically; with CaptureErrorAsync you can set them
yourself — the dashboard surfaces the exception type and inner-exception chain on the issue page.
await _client.CaptureErrorAsync(new ErrorData
{
Message = "Custom error message",
Severity = 3, // 0=Debug, 1=Info, 2=Warning, 3=Error, 4=Critical
ExceptionType = "MyApp.Domain.OrderValidationException",
InnerException = "Npgsql.NpgsqlException: Failed to connect to <db-host>:5432",
Metadata = System.Text.Json.JsonSerializer.Serialize(new
{
CustomField = "Value",
UserAction = "Upload"
})
});
5. Flush on Application Shutdown
Automatic since v1.3.0 — the client registers a hosted service that flushes buffered errors on shutdown (and hooks AppDomain.UnhandledException / TaskScheduler.UnobservedTaskException), so you don't need to wire anything up. You can still flush manually if you have a special case:
var client = app.Services.GetRequiredService<ICherryBoardClient>();
await client.FlushAsync();
Configuration Validation
All configuration options are validated during AddCherryBoard() registration. If any values are outside their valid ranges, an ArgumentException will be thrown immediately with a clear error message.
Example validation errors:
// ❌ Error: MaxBatchSize must be between 1 and 1000
builder.Services.AddCherryBoard(options =>
{
options.MaxBatchSize = 5000; // Too large!
});
// ❌ Error: SampleRate must be between 0.0 and 1.0
builder.Services.AddCherryBoard(options =>
{
options.SampleRate = 1.5; // Invalid!
});
// ✅ Valid configuration
builder.Services.AddCherryBoard(options =>
{
options.MaxBatchSize = 100; // Valid: 1-1000
options.SampleRate = 0.5; // Valid: 0.0-1.0
options.MaxQueueItems = 5000; // Valid: 1-1,000,000
});
Capturing Background Jobs & Logged Errors (v1.1.0+)
The automatic middleware only sees unhandled exceptions on the HTTP request pipeline. Errors in background jobs (e.g. Hangfire), hosted services, or anything that's caught and logged never reach it. AddCherryBoardLogging() closes that gap by forwarding log entries to CherryBoard app-wide.
builder.Services.AddCherryBoard(options => { /* ... */ });
// Forward logs at or above MinimumLogLevel (default: Warning) to CherryBoard.
builder.Logging.AddCherryBoardLogging();
Once registered you need no per-call-site code — just log normally:
// In a Hangfire job, IHostedService, or anywhere the HTTP middleware can't reach:
_logger.LogError(ex, "Failed to upload file {File}", fileName); // → captured automatically
What each layer captures
| Layer | Captures | Use for |
|---|---|---|
app.UseCherryBoard() (middleware) |
Unhandled exceptions on HTTP requests, + request path/method/user/IP | Web request errors |
builder.Logging.AddCherryBoardLogging() |
Any LogWarning/LogError/LogCritical app-wide — jobs, hosted services, caught-and-logged |
Non-HTTP code paths |
ICherryBoardClient.CaptureExceptionAsync / CaptureErrorAsync |
Explicit reports with custom metadata | Custom context |
Log levels map to severity: Critical → Critical, Error → Error, Warning → Warning.
Built-in safeguards:
- No recursion — the integration ignores its own log output, so a failed send can't loop.
- No duplicates — an unhandled HTTP exception captured by the middleware is not re-captured via its log entry.
- Non-blocking — entries are queued (respecting
SampleRate); logging never waits on the network.
Request Timing (v1.4.0+)
Measures how long each request takes to process and reports per-route rollups to the dashboard, where they appear next to the browser SDK's numbers for the same route.
Off by default. Two things to enable it:
builder.Services.AddCherryBoard(options =>
{
options.ApiKey = "your-api-key-here";
options.ApiUrl = "https://your-cherryboard-host";
options.EnablePerformanceTracking = true;
});
var app = builder.Build();
// Register early. Whatever runs before this is not timed, so putting it near the
// top of the pipeline measures the request as the caller experienced it rather
// than just your handler.
app.UseCherryBoardPerformance();
Timings appear on the environment page in the dashboard within a few minutes.
Why measure both sides
The browser SDK already reports how long API calls take. That number includes DNS, TLS, the network and transferring the response; this one is server processing alone.
Neither answers the question by itself, but the gap between them does:
| What you see | What it points at |
|---|---|
| Both numbers similar | Your code — the time is being spent in the handler |
| Frontend much higher | Payload size or the network, not the handler |
| Backend high, few calls | A slow dependency on an endpoint nobody hits often |
What it costs
A Stopwatch per request and an in-memory counter update. Rollups are posted on
a timer, so the payload is bounded by how many endpoints the app has rather than
how much traffic it serves — a service handling a million requests sends the same
small summary as one handling a hundred.
Delivery failures are swallowed by design. A dropped window is a gap in a chart; a thrown exception would be an outage.
What is not recorded
Responses the server deliberately holds open are skipped: server-sent events
(text/event-stream) and protocol upgrades (HTTP 101, e.g. WebSockets).
For these, elapsed time is how long the client stayed subscribed, not how long anything took to compute. Recorded as latency it is not merely noisy but wrong — and being by far the largest number, a perfectly healthy stream would sit permanently at the top of the slowest-routes table and drag the environment's overall percentile with it.
Detection is by media type and status code, so this applies to any streaming endpoint without needing to be configured.
For anything else you want left out — long-polling, large downloads, an endpoint that is slow by design — mark it:
[HttpGet("download/{id}")]
[CherryBoardIgnorePerformance]
public async Task<IActionResult> Download(Guid id) { ... }
It works on a whole controller too, and is inherited. Use it sparingly: a route that is genuinely slow is the thing this feature exists to show you. Exclude the ones where elapsed time is not latency, not the ones you would rather not see.
Note the browser SDK measures streams differently: it reads only fetch and
XHR resource timings, so EventSource never appears. A fetch() that streams
its body would be measured until the stream closes, and would be over-counted
the same way.
What gets recorded
Paths are normalized before anything is stored, so /api/orders/12345 becomes
/api/orders/:id. This deliberately uses the same rules as the browser SDK
rather than ASP.NET's matched route template: the two sides have to produce
identical strings for the comparison above to work at all.
- Query strings are never recorded — they carry tokens and email addresses, and say nothing about how long a request took.
- 404s group under a single label, since the set of paths nobody serves is unbounded.
4xxis not counted as an error. A caller's bad request is not your endpoint failing, and counting it would make validation look like breakage.
How It Works
Automatic Capture: The middleware intercepts unhandled exceptions, enriches them with HTTP context (request path, method, IP, user agent, etc.), and sends them asynchronously to CherryBoard Dashboard.
Continuous delivery (v1.3.0+): Captured errors are written to a bounded in-memory queue and a single background worker sends them continuously (near real-time), batching only what has accumulated. The queue is flushed on application shutdown, so buffered errors aren't lost on deploy/restart.
Retry Logic: Failed requests are automatically retried with exponential backoff (2^attempt seconds).
Sampling: Control error volume by setting a sample rate (e.g., 0.5 = 50% of errors).
Non-blocking: Capturing never blocks your application — it enqueues and returns immediately (and drops, rather than blocks, if the queue is full).
Architecture
Application Exception / LogError / unhandled exception
↓
Capture → bounded in-memory queue (drop-when-full)
↓
Background worker (continuous, batched)
↓
Send to API with Retry ← flushed on shutdown
↓
CherryBoard Dashboard
Best Practices
Set Appropriate Sample Rate: For high-traffic applications, consider setting
SampleRateto0.1(10%) to reduce volume.Configure Environment: Always set the
Environmentoption to distinguish between Production, Staging, and Development errors.Flush on Shutdown: Handled automatically since v1.3.0 — no manual registration needed.
Don't Catch and Ignore: Let exceptions bubble up so the middleware can capture them, or manually capture before handling.
License
MIT
Support
For issues or questions, visit GitHub Issues
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Http.Abstractions (>= 2.3.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Http (>= 10.0.1)
- Microsoft.Extensions.Options (>= 10.0.1)
-
net8.0
- Microsoft.AspNetCore.Http.Abstractions (>= 2.3.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Http (>= 10.0.1)
- Microsoft.Extensions.Options (>= 10.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.