Tillväga 1.0: Att göra Umami | . | NET-produktion redo
Inleiding
När jag först integrerade mig Umamianalys in i min bloggplattform.,. Jag sprang snabbt in i en frustrerande verklighet, :. Umami. '. API-dokumentationen är: .... Låt '. vara välgörenhetsfull och kalla det """. Minimalt.
Så jag byggde Umami.NET - inte bara som en enkel HTTP-wrapper, ,, utan också som en produktions-, ready client library som kompenserar för alla Umamis knorr.
- Den komplexa valideringen med hjälpsamma felvaror
- Robust testinfrastruktur
- Ganska bra handling av fel för riktiga scenarior
Låt mig gå igenom vad som gör den här bibliotekets produktion
Problemet med dokumentationen
Här är vad du står inför när du arbetar med Umamis API direkt.
- Ingen inmatningsvalidation - Senda en missformerad GUID
- Kriptiska reaktioner - Bot upptäckts? Du får
"beep boop". Det där ' det är det - Att bryta mot förändringar - Parameter omvandlade mellan versioner
pathmoturl,hostnamemothost) - Tidstampen förvirring - Enix millisekunder ? sekunder M SK2 Lycka till att klura ut det
- JWT: s svar - Ibland fulla payloads , ibland bara en besökeridentifikation. Ingen dokumentation som förklarar när eller varför
Det är bra för en snabb prototyp, men för produktionen behöver du något bättre.
Guardklagor : Misslyckas snabbt med sammanhang
De värsta bugarna är de som inte lyckas tyst.
KonfigurationValidering
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");
}
Den körs vid uppstart i din dator Program.cs. Om din konfiguration är felaktiga, så vet du omedelbart - inte när det första analyseret försöker skicka
Tillåt att bevalidera med hjälpsamma förslag
Men den verkliga magin finns i sökstringassistenten.
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);
}
}
}
}
Notera att Suggestion: prefix? Varje fel meddelande säger till dig vad som gick fel och hur man fixar det. Detta är dokumentationen som Umami borde ha tillhandahållit
Datumsriktvalidation
När man bygger analytiska queryr
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).");
}
}
Att hantera Umami's kvävar
Problemet med "Beep Boop
Umami's botdetektor ger en enkel textrespons "beep boop". Inte JSON
Här är hur Umami handleder det.
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);
}
}
Din kod får en ren enum:
public enum ResponseStatus
{
Failed,
BotDetected,
Success
}
Det finns inga fler konstiga svar att analysera - bara kolla statusen
Parameterns namn ändras
Umami omvandlade parametrar mellan API-versioner. Dokumenterade de det här? ? Självklart inte.
// Support both old and new parameter names
request.Path = queryParams["path"] ?? queryParams["url"];
request.Hostname = queryParams["hostname"] ?? queryParams["host"];
Timestamp-konversioner
Umami använder Unix millisekunder för tidstämpning.
public static long ToMilliseconds(this DateTime dateTime)
{
var dateTimeOffset = new DateTimeOffset(dateTime.ToUniversalTime());
return dateTimeOffset.ToUnixTimeMilliseconds();
}
Nu kan du arbeta med normala DateTime objekt och låter biblioteket hantera omvandlingen.
bakgrundsprocessering med kanaler
Analytik borde aldrig blockera din application. Umami.NET inkluderar en bakgrundssändare som använder 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");
}
}
}
}
Erfarenheter är i köer i minnet och bearbetas asynkroniskt. Dina webbansökningar återvänder omedelbart , analytik händer i bakgrunden
Försök om reglerna med Polly
nätverkliga misslyckanden händer. Biblioteket använder Polly för motståndskraftiga HTTP sändningar:
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);
}
Innangripande misslyckanden och 503 fel utlöser automatiska återvändringar med exponentiell bakgång . Dina analytiker är motståndskraftiga mot tillfälliga nätverksproblem
Authentisering med Auto-Retry
När man tar fram analytiska data ( inte bara skickar händelser), behöver du autentiseringenM SK2 Biblioteket hanterar tokens utlösning automatiskt :
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);
}
Man behöver aldrig tänka på att hantera token.
Testning av infrastruktur
Tillverkning- färdig kod behöver omfattande tester . Här' det är vad jag byggde
FakeLogger för logkontroll
Med hjälp av Microsoft's FakeLogger package, tester kan kontrollera logeringsbeteende
[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));
}
Custom Mock HTTP Handlers
Att testa async operationer är klurigt. 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
}
Detta mönster garanterar:
- bakgrundshändelser som faktiskt processas
- Bearbetandet slutar inom en rimlig tid
- Ägningar i den modellbehandlare rapporteras ordentligt
Omgripande testbeläggning
Testpaketet täcker::
- ✅Konfigurationsvalidation (invalide GUIDs, sakna URLs)
- ✅ Att följa händelser med och utan data
- ✅ Stronbildspårning
- ✅ Användaridentifikation
- ✅ Bot-detektorbehandling
- ✅ JWT response decoding
- ✅ bakgrundsbearbetande med avbrott
- ✅ Datumsrikt validering
- ✅ Generering av sökord
- ✅ Authentisering och tokensuppfriskning
- ✅ Metrik och dataupplärning av webbsidor
Verklig-Worlda användande
Här är hur enkelt det är att använda i ett ASP-program
Konfiguration i program.cs
builder.Services.SetupUmamiClient(builder.Configuration);
Det' är det . Biblioteket läser din appsettings.json:
{
"Analytics": {
"UmamiPath": "https://analytics.yoursite.com",
"WebsiteId": "your-website-guid"
}
}
Träg händelser
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");
}
}
Fetch Analytics Data
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");
}
}
Vad är nästa steg för 1.0?
Biblioteket är ett feature-komplett och stridsfullt -testat i produktion på just den här bloggen
- 📝 Omgripande API-dokumentation
- 🎯 NuGet paketpublicering
- 📊 Leistungsindikatorer
- 🔧 Weitere bekvämlighetsmetoder för vanliga analytiska frågor
Slutsatsen
Att bygga en produktionslig bibliotek är inte bara att lägga ihop ett API, det handlar om att skapa en upplevelse som bättre som att använda API direkt.
- Validering som förklarar vad som gick fel och hur man löser det
- God handling av märkliga API-beteende
- Kompleksa tester som bevisar att det fungerar
- Hintergrundbehandling som inte blockerar din app
- Robust handling av fel med automatiska repetitioner
Om du använder Umami analytik i en NET-app Umami.NET. Den ' är öppen källkod , hårdtestad M SK3 och designad för att göra ditt liv lättare
Jag har frågor eller förslag? Öppna en fråga på GitHub eller kontakta mig i kommentarerna härunder