This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Vers 1.0: Faire prêt la production de Umami.NET
Thursday, 20 November 2025
Introduction
Lorsque j'ai intégré pour la première fois Analyse Umami dans ma plate-forme de blog, J’ai rapidement affronté une réalité frustrante : Umami' la documentation API de ... permet à 's d’être caritatif et de l’appeler MSC5minimalM SK6 Les messages d’erreur sont cryptiques au meilleur des casMNK7 nonMRK8existants au pire des cas~. Les paramètres changent entre les versions sans avertissement MNK10 Et ne m’amènez pas non plus à commencer sur le "beep boop~MNK13 réponse de détection du botMGK14
J'ai donc construit Umami.NET - non seulement comme un enveloppeur HTTP simple, mais aussi comme une bibliothèque de clients prêts à produire qui compense toutes les erreurs d’Umami.
- Validation globale avec des messages d'erreur utiles
- Infrastructure d'essai robuste
- Traitement gracieux des erreurs pour les scénarios de real-world
Permettez-moi de vous expliquer ce qui fait que cette bibliothèque soit produite.
Le problème: Umami's Gap de documentation
Voici ce à quoi vous êtes confrontés lorsque vous travaillez directement avec Umami.
- Aucune validation de l'entrée - Envoyer un GUID malformé? Défaut silencieux .
- Réponses cripticales - Bot détecté? Vous obtiendrez
"beep boop". Il s’agit d’un ', c’est-à-dire un . - Résilier les changements - Paramètres renommés entre les versions
pathcontreurl,hostnamecontrehost) - Confusion de l'échantillonnage du temps - Unix millisecondes
- Réponses du JWT - Parfois les charges de paye complètes , parfois seulement une carte d’identité du visiteur. Aucune documentation pour expliquer quand ou pourquoi
Ce n’est pas mal pour un prototype rapide, mais pour la production? Vous avez besoin d’une chose meilleureM SK2
Clauses de garde: Défaut rapide avec le contexte
Les plus graves sont ceux qui échouent silencieusement. Umami.NET saisit des erreurs de configuration lors du démarrage avant qu'elles ne puissent causer des problèmes dans la production
Configuration Validation
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");
}
Cela fonctionne au début de votre système. Program.cs. Si votre configuration est incorrecteM SK1 vous savez immédiatement - que le premier événement d'analyse essaie de transmettre
Demande de validation avec des suggestions utiles
Mais la vraie magie se trouve dans l’aide à la chaîne de recherche. Vérifiez ces messages d’erreur:
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);
}
}
}
}
Veuillez noter que Suggestion: préfixe? Chaque message d'erreur vous dit ce qui s'est passé mal et comment le corriger. Il s'agit de la documentation que Umami aurait dû fournir.
Validation de la portée des dates
Lorsque vous construisez des demandes d’analyse, les intervalles de dates peuvent être difficilesM SK1 La bibliothèque prend ces erreurs pour vous:
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).");
}
}
Traitement des erreurs de l’Omami'
Le problème "Beep Boop"
La détection du bot Umami's retourne une réponse de texte simple "beep boop". Non JSONM SK1 Pas de code d'état approprié . JusteMSC3 beep boop.
Voici la façon dont ', Umami, . et NET le gèrent.
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);
}
}
Votre code obtient un enum propre:
public enum ResponseStatus
{
Failed,
BotDetected,
Success
}
Il n'y a plus de réponses étranges à parler - vérifiez simplement le statutM SK1
Changements de nom des paramètres
Umami a renouvelé les paramètres entre les versions API. On y a-t-il documenté?
// Support both old and new parameter names
request.Path = queryParams["path"] ?? queryParams["url"];
request.Hostname = queryParams["hostname"] ?? queryParams["host"];
Conversions de Timestamp
Umami utilise les millisecondes Unix pour le timbre de temps.
public static long ToMilliseconds(this DateTime dateTime)
{
var dateTimeOffset = new DateTimeOffset(dateTime.ToUniversalTime());
return dateTimeOffset.ToUnixTimeMilliseconds();
}
Maintenant, vous pouvez travailler avec un ordinaire DateTime et permettre à la bibliothèque de gérer la conversion.
Traitement du contexte avec les canaux
Analytics ne doit jamais bloquer votre application. Umami.NET comprend un éditeur de fond utilisant 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");
}
}
}
}
Les événements sont en rangée dans la mémoire et traités asynchronement. vos demandes Web retournent instantanément, les analyses se déroulent dans l'arrière-plan .
Essayer de nouveau les politiques avec Polly
Des pannes de réseau se produisent. La bibliothèque utilise Polly pour les appels HTTP résistants:
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);
}
Les défaillances transitoires et les erreurs 503 déclenchent des répétitions automatiques avec un ralentissement exponentiel.
Authentication avec Auto-Retry
Lors de l'acheminement des données d'analyse (non seulement envoyant les événementsM SK1 vous avez besoin d'authentification. La bibliothèque gère automatiquement l'expiration du token
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);
}
Il n’est jamais nécessaire de penser à la gestion des tokens.
Examen de l'infrastructure
Production-le code prêt nécessite des essais completsM SK1C'est là'ce que j'ai construitMSC3
FakeLogger pour la vérification du log
Utiliser Microsoft's FakeLogger package, les essais peuvent vérifier le comportement d’enregistrement:
[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));
}
Les gestionnaires HTTP Mock personnalisés
Il est difficile de tester les opérations asynchrones. 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
}
Ce modèle garantit:
- Les événements de fond sont en fait traités
- Le traitement est terminé dans un délai raisonnable
- Les affirmations dans le gestionnaire de modèle sont correctement signalées
Couverture d'essai approfondie
La suite d'essai couvre:
- ✅ Validation de la configuration
- ✅ Traçage des événements avec et sans données
- ✅ Traçage de la page
- ✅ Identification de l'utilisateur
- ✅ Manipulation de la détection des bottes
- ✅ Décodage de la réponse du JWT
- ✅ Traitement de fond avec délais
- ✅ Validation de la gamme des dates
- ✅ Production de chaînes de demande
- ✅ Authentification et rafraîchissement du token
- ✅ Recouvrement des données sur les métriques et les vues de page
Utilisation mondiale
Voici'à quel point il est simple d’utiliser une application ASP.NET CoreM SK2
Établissement dans le programme.cs
builder.Services.SetupUmamiClient(builder.Configuration);
Que'c'est-ce. La bibliothèque lit votre appsettings.json:
{
"Analytics": {
"UmamiPath": "https://analytics.yoursite.com",
"WebsiteId": "your-website-guid"
}
}
Événements de la piste
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");
}
}
Extraire les données d'analyse
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");
}
}
Qu'est-ce que 's Suivant pour 1.0?
La bibliothèque est caractéristiques-complet et bataille -testé en production sur ce même blogM SK2 Avant la 1.0 mise à jourMNK4 IMRK5m axée surMMK6
- 📝 Documentation API globale
- 🎯 Publication de paquets NuGet
- 📊 Critères de rendement
- 🔧 Méthodes de commodité additionnelles pour les demandes d'analyse commune
Conclusion
La création d’une bibliothèque prête pour la production n’est pas seulement une question de remplir un API, mais aussi de créer une expérience améliorer par rapport à l’utilisation directe de l’API.
- Validation qui explique ce qu'il y a eu de mal et comment le corriger
- Traitement gracieux des comportements API étranges
- Essais exhaustifs qui prouvent qu'il fonctionne
- Traitement du contexte qui ne bloque pas votre app
- Traitement d'erreurs résistants avec reprises automatiques
Si vous 'utilisez les analyses Umami dans une application .NET Umami.NET. C’est la source ouverte de ', fortement testéeM SK3 et conçue pour faciliter votre vie
Questions ou suggestions reçues? Ouvrez un numéro sur GitHub ou communiquez avec les commentaires ci-dessous!