Kun ensimmäisen kerran kotouduin Umami-analytiikka Bloggausalustalleni törmäsin nopeasti turhauttavaan todellisuuteen: Umamin API-dokumentaatio on... ollaan hyväntahtoisia ja kutsutaan sitä "minimaaliseksi". Virheviestit ovat parhaimmillaan kryptisiä, pahimmillaan olemattomia. Parametrit muuttuvat versioiden välillä ilman varoitusta. Äläkä edes saa minua aloittamaan "beep boop" bot -havaitsemisreaktiota.
Joten rakensin Umami.NET - ei vain yksinkertaisena HTTP-paperina, vaan tuotantovalmiina asiakaskirjastoina, jotka korvaavat kaikki Umamin oikkuja. Työskennellessäni kohti 1,0-julkaisua olen keskittynyt kolmeen kriittiseen alueeseen:
Kerron, mikä tekee tästä kirjaston tuotantovalmista.
Umamin API:n kanssa on vastassa tämä:
"beep boop"Siinä kaikki.path vs. url, hostname vs. host)Tämä sopii nopeaan prototyyppiin, mutta tuotantoon tarvitaan jotain parempaa.
Pahimpia vikoja ovat ne, jotka epäonnistuvat äänettömästi. Umami.NET nappaa kokoonpanovirheitä käynnistyksessä, ennen kuin ne voivat aiheuttaa ongelmia tuotannossa.
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");
}
Tämä tapahtuu startup-tilassa Program.cs. Jos kokoonpanosi on väärä, tiedät heti - ei silloin, kun ensimmäinen analytiikkatapahtuma yrittää lähettää.
Todellinen taikuus on kuitenkin hakujonon auttajassa. Katso näitä virheviestejä:
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);
}
}
}
}
Huomaa, että Suggestion: Ennuste? Jokainen virheilmoitus kertoo mikä meni pieleen sekä miten se korjataanUmamin olisi pitänyt toimittaa nämä asiakirjat.
Analytiikkakyselyitä laadittaessa treffivalikoimat voivat olla hankalia. Kirjasto havaitsee nämä virheet:
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).");
}
}
Umamin bottihavaitseminen palauttaa selkeän tekstivastauksen: "beep boop"Ei JSON, ei kunnon statuskoodi.
Näin Umami.net hoitaa asian:
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);
}
}
Koodisi saa puhtaan enum:
public enum ResponseStatus
{
Failed,
BotDetected,
Success
}
Ei enää outoja vastauksia - tarkista vain tilanne.
Umami nimesi API-versioiden väliset muuttujat uudelleen. Dokumentoivatko ne tämän? Ei tietenkään. Kirjasto käsittelee molempia:
// Support both old and new parameter names
request.Path = queryParams["path"] ?? queryParams["url"];
request.Hostname = queryParams["hostname"] ?? queryParams["host"];
Umami käyttää Unix-millisekuntia aikaleimoissa. Tässä on auttaja, joka tekee siitä kivuttoman:
public static long ToMilliseconds(this DateTime dateTime)
{
var dateTimeOffset = new DateTimeOffset(dateTime.ToUniversalTime());
return dateTimeOffset.ToUnixTimeMilliseconds();
}
Nyt voit työskennellä normaalin kanssa DateTime Esineitä ja anna kirjaston hoitaa remontointi.
Analyytikot eivät saa koskaan estää sovellustasi. Umami.NET sisältää taustan lähettäjän, joka käyttää 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");
}
}
}
}
Tapahtumat jonotetaan muistiin ja käsitellään epäyhtenäisesti. Nettipyyntösi palaavat välittömästi, analytiikka tapahtuu taustalla.
Verkkoviat tapahtuvat. Kirjasto käyttää Pollya kestäviin HTTP-puheluihin:
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);
}
Ohimenevät viat ja 503 virhettä laukaisevat automaattisia retriisejä eksponentiaalisella takaiskulla. Analyytikot kestävät tilapäisiä verkkokysymyksiä.
Kun haet analytiikkatietoja (ei vain tapahtumien lähettämistä), tarvitset todennuksen. Kirjasto käsittelee symbolisen vanhenemisen automaattisesti:
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);
}
Ei koskaan tarvitse ajatella näennäistä johtamista, se vain toimii.
Tuotantovalmiiden koodien täytyy olla kattavia testejä. Tämän rakensin:
Microsoftin FakeLogger paketti, testeillä voidaan todentaa puunkorjuukäyttäytyminen:
[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));
}
Async-toimintojen testaaminen on hankalaa. Tässä kuvio, jossa käytetään 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
}
Tällä kuviolla varmistetaan:
Testisarja kattaa:
-
ASP.NET Core -sovelluksen käyttö on näin yksinkertaista:
builder.Services.SetupUmamiClient(builder.Configuration);
Kirjastossa lukee: 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");
}
}
Ennen 1.0-julkaisua keskityn seuraaviin aiheisiin:
Tuotantovalmian kirjaston rakentamisessa ei ole kyse vain API:n käärimisestä vaan siitä, että luodaan kokemus, joka on parempi Umami.NET korvaa Umamin dokumentointipuutteet:
Jos käytät Umami-analytiikkaa .net-sovelluksessa, haluaisin, että kokeilet Umami.NETSe on avoin lähdekoodi, hyvin testattu ja suunniteltu helpottamaan elämääsi.
Onko sinulla kysyttävää tai ehdotuksia? Avaa kysymys GitHubista tai ota yhteyttä alla oleviin kommentteihin!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.