Toen ik voor het eerst integreerde Umami-analyse in mijn blog platform, Ik liep snel in een frustrerende realiteit: Umami's API documentatie is... laten we liefdadigheid en noem het "minimaal." Foutmeldingen zijn cryptisch op zijn best, niet-bestaand op het slechtst. Parameters veranderen tussen versies zonder waarschuwing. En laat me niet eens beginnen op de "piep boop" bot detectie reactie.
Dus ik bouwde Umami.NET - niet alleen als een eenvoudige HTTP wrapper, maar als een productie-ready client library die compenseert voor al Umami's eigenaardigheden. Aangezien ik heb gewerkt aan een 1.0 release, ik heb me gericht op drie kritieke gebieden:
Laat me je vertellen wat deze bibliotheek productie-klaar maakt.
Dit is waar je mee te maken hebt als je rechtstreeks met Umami's API werkt:
"beep boop"Dat is het.path vs url, hostname vs host)Dit is prima voor een snel prototype, maar voor de productie? Je hebt iets beters nodig.
De ergste bugs zijn degenen die stil falen. Umami.NET vangt configuratiefouten bij het opstarten voordat ze problemen in de productie kunnen veroorzaken.
public static void ValidateSettings(UmamiClientSettings settings)
{
// Guard: UmamiPath is required
if (string.IsNullOrEmpty(settings.UmamiPath))
throw new ArgumentNullException(settings.UmamiPath,
"UmamiUrl is required");
// Guard: UmamiPath must be valid URI
if (!Uri.TryCreate(settings.UmamiPath, UriKind.Absolute, out _))
throw new FormatException(
"UmamiUrl must be a valid Uri");
// Guard: WebsiteId is required
if (string.IsNullOrEmpty(settings.WebsiteId))
throw new ArgumentNullException(settings.WebsiteId,
"WebsiteId is required");
// Guard: WebsiteId must be valid GUID
if (!Guid.TryParseExact(settings.WebsiteId, "D", out _))
throw new FormatException(
"WebSiteId must be a valid Guid");
}
Dit loopt bij het opstarten in uw Program.cs. Als uw configuratie fout is, weet u het direct - niet wanneer de eerste analytics event probeert te verzenden.
Maar de echte magie zit in de query string helper. Bekijk deze foutmeldingen:
public static string ToQueryString(this object obj)
{
if (obj == null)
{
throw new ArgumentNullException(nameof(obj),
"Cannot convert null object to query string. " +
"Suggestion: Ensure you create and populate a request object " +
"before calling ToQueryString().");
}
foreach (var property in objectType.GetProperties())
{
if (attribute.IsRequired)
{
if (propertyValue == null)
{
throw new ArgumentException(
$"Required parameter '{propertyName}' " +
$"(property '{property.Name}') cannot be null. " +
$"Suggestion: Set the {property.Name} property " +
$"on your {objectType.Name} object...",
property.Name);
}
// For strings, check for empty/whitespace
if (propertyValue is string strValue &&
string.IsNullOrWhiteSpace(strValue))
{
throw new ArgumentException(
$"Required parameter '{propertyName}' " +
$"cannot be empty or whitespace. " +
$"Suggestion: Set {property.Name} to a valid non-empty value.",
property.Name);
}
}
}
}
Nota van de Suggestion: Voorvoegsel? Elke foutmelding vertelt u Wat is er misgegaan? en hoe het te repareren. Dit is de documentatie die Umami zou moeten hebben verstrekt.
Bij het bouwen van analytische queries, kunnen datumbereiken lastig zijn. De bibliotheek vangt deze fouten voor u op:
public DateTime StartAtDate
{
get => _startAtDate;
set
{
if (_endAtDate != default && value > _endAtDate)
{
throw new ArgumentException(
$"StartAtDate ({value:O}) must be before EndAtDate ({_endAtDate:O}). " +
"Suggestion: Set StartAtDate to an earlier date or adjust EndAtDate.",
nameof(StartAtDate));
}
_startAtDate = value;
}
}
public virtual void Validate()
{
if (StartAtDate == default)
{
throw new InvalidOperationException(
"StartAtDate is required. " +
"Suggestion: Set StartAtDate to a valid date " +
"(e.g., DateTime.UtcNow.AddDays(-7) for last 7 days).");
}
}
Umami's bot detectie geeft een platte tekst reactie terug: "beep boop"Niet JSON, geen echte statuscode.
Dit is hoe Umami.NET ermee omgaat:
public async Task<UmamiDataResponse> DecodeResponse(HttpResponseMessage response)
{
var responseString = await response.Content.ReadAsStringAsync();
// Handle bot detection
if (responseString.Contains("beep") && responseString.Contains("boop"))
{
logger.LogWarning("Bot detected - data not stored in Umami");
return new UmamiDataResponse(ResponseStatus.BotDetected);
}
// Handle JWT response
try
{
var jwtPayload = DecodeJwt(responseString);
return new UmamiDataResponse(ResponseStatus.Success, jwtPayload);
}
catch (Exception e)
{
logger.LogError(e, "Failed to decode response");
return new UmamiDataResponse(ResponseStatus.Failed);
}
}
Uw code krijgt een schoon enum:
public enum ResponseStatus
{
Failed,
BotDetected,
Success
}
Geen rare antwoorden meer ontleden - controleer gewoon de status.
Umami hernoemde parameters tussen API-versies. Hebben ze dit gedocumenteerd? Natuurlijk niet. De bibliotheek behandelt beide:
// Support both old and new parameter names
request.Path = queryParams["path"] ?? queryParams["url"];
request.Hostname = queryParams["hostname"] ?? queryParams["host"];
Umami gebruikt Unix milliseconden voor tijdstempels. Hier is een helper die het pijnloos maakt:
public static long ToMilliseconds(this DateTime dateTime)
{
var dateTimeOffset = new DateTimeOffset(dateTime.ToUniversalTime());
return dateTimeOffset.ToUnixTimeMilliseconds();
}
Nu kun je met normaal werken. DateTime objecten en laat de bibliotheek de conversie afhandelen.
Analytics mag uw toepassing nooit blokkeren. Umami.NET bevat een achtergrondzender die gebruik maakt van System.Threading.Channels:
public class UmamiBackgroundSender : IHostedService
{
private readonly Channel<UmamiPayload> _channel;
private readonly UmamiClient _client;
public async Task Track(string eventName,
string? url = null,
UmamiEventData? data = null)
{
var payload = new UmamiPayload
{
Website = _settings.WebsiteId,
Name = eventName,
Url = url ?? string.Empty,
Data = data
};
// Non-blocking write to channel
await _channel.Writer.WriteAsync(payload);
}
private async Task ProcessQueue(CancellationToken stoppingToken)
{
await foreach (var payload in _channel.Reader.ReadAllAsync(stoppingToken))
{
try
{
await _client.Send(payload);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send event to Umami");
}
}
}
}
Gebeurtenissen worden in de wachtrij in het geheugen geplaatst en asynchroon verwerkt. Uw webverzoeken keren onmiddellijk terug, analytics gebeuren op de achtergrond.
Netwerkstoringen gebeuren. De bibliotheek gebruikt Polly voor veerkrachtige HTTP-oproepen:
public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
var delay = Backoff.DecorrelatedJitterBackoffV2(
TimeSpan.FromSeconds(1),
retryCount: 3);
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.ServiceUnavailable)
.WaitAndRetryAsync(delay);
}
Voorbijgaande storingen en 503 fouten leiden tot automatische herhalingen met exponentiële backoff. Uw analyses zijn veerkrachtig voor tijdelijke netwerkproblemen.
Bij het ophalen van analytics-gegevens (niet alleen versturen van evenementen), hebt u authenticatie nodig. De bibliotheek behandelt het verlopen van token automatisch:
public async Task<UmamiResult<StatsResponseModel>> GetStats(StatsRequest statsRequest)
{
var response = await _httpClient.GetAsync(url);
// Token expired? Re-authenticate and retry
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await _authService.Login();
return await GetStats(statsRequest); // Recursive retry
}
// Parse and return
var content = await response.Content.ReadFromJsonAsync<StatsResponseModel>();
return new UmamiResult<StatsResponseModel>(
response.StatusCode,
response.ReasonPhrase ?? string.Empty,
content);
}
Je hoeft nooit na te denken over token management - het werkt gewoon.
Productie-ready code moet uitgebreide tests. Hier is wat ik gebouwd:
Microsoft's gebruiken FakeLogger pakket, testen kunnen loggedrag verifiëren:
[Fact]
public async Task Login_Success_LogsMessage()
{
// Arrange
var fakeLogger = new FakeLogger<AuthService>();
var authService = new AuthService(httpClient, settings, fakeLogger);
// Act
await authService.Login();
// Assert
var logs = fakeLogger.Collector.GetSnapshot();
Assert.Contains("Login successful", logs.Select(x => x.Message));
}
Het testen van async operaties is lastig. Hier is een patroon met behulp van TaskCompletionSource:
[Fact]
public async Task BackgroundSender_ProcessesEventAsynchronously()
{
var tcs = new TaskCompletionSource<bool>();
var handler = EchoMockHandler.Create(async (message, token) =>
{
try
{
// Assert the request was sent correctly
var payload = await message.Content.ReadFromJsonAsync<UmamiPayload>();
Assert.Equal("test-event", payload.Name);
tcs.SetResult(true); // Signal test completion
return new HttpResponseMessage(HttpStatusCode.OK);
}
catch (Exception e)
{
tcs.SetException(e);
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
});
// Track event
await backgroundSender.Track("test-event");
// Wait for background processing with timeout
var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(1000));
if (completedTask != tcs.Task)
throw new TimeoutException("Event was not processed within timeout");
await tcs.Task; // Throw if assertions failed
}
Dit patroon zorgt voor:
De test suite heeft betrekking op:
Hier is hoe eenvoudig het is om te gebruiken in een ASP.NET Core applicatie:
builder.Services.SetupUmamiClient(builder.Configuration);
Dat is het, de bibliotheek leest jouw appsettings.json:
{
"Analytics": {
"UmamiPath": "https://analytics.yoursite.com",
"WebsiteId": "your-website-guid"
}
}
public class HomeController : Controller
{
private readonly UmamiBackgroundSender _umami;
public HomeController(UmamiBackgroundSender umami)
{
_umami = umami;
}
public IActionResult Index()
{
// Non-blocking event tracking
await _umami.TrackPageView("/", "Home Page");
return View();
}
[HttpPost]
public async Task<IActionResult> Subscribe(string email)
{
// Track with custom data
await _umami.Track("newsletter-signup",
data: new UmamiEventData
{
{ "source", "homepage" },
{ "email_domain", email.Split('@')[1] }
});
return RedirectToAction("ThankYou");
}
}
public class AnalyticsDashboardController : Controller
{
private readonly IUmamiDataService _umamiData;
public async Task<IActionResult> Stats()
{
var request = new StatsRequest
{
StartAtDate = DateTime.UtcNow.AddDays(-30),
EndAtDate = DateTime.UtcNow
};
var result = await _umamiData.GetStats(request);
if (result.Status == HttpStatusCode.OK)
{
var stats = result.Data;
// stats.Visitors, stats.PageViews, stats.BounceRate, etc.
return View(stats);
}
return View("Error");
}
}
De bibliotheek is feature-complete en battle-getest in productie op deze zeer blog. Voor de 1.0 release, ik ben gericht op:
Het bouwen van een productie-ready bibliotheek gaat niet alleen over het inpakken van een API - het gaat over het creëren van een ervaring die's beter dan de API rechtstreeks te gebruiken. Umami.NET compenseert Umami's documentatie hiaten met:
Als je Umami analytics gebruikt in een .NET applicatie, zou ik graag willen dat je het probeert. Umami.NETHet is open source, zwaar getest, en ontworpen om je leven gemakkelijker te maken.
Heb je vragen of suggesties? Open een probleem op GitHub of reik uit in de reacties hieronder!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.