Back to "Kohti 1.0: Making Umami.NET Production Ready"

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

Analytics C# Open Source Umami

Kohti 1.0: Making Umami.NET Production Ready

Thursday, 20 November 2025

Johdanto

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:

  1. Kattava validointi ja hyödylliset virheilmoitukset
  2. Voimakas testausinfrastruktuuri
  3. Armollinen virheiden käsittely tosimaailman skenaarioissa

Kerron, mikä tekee tästä kirjaston tuotantovalmista.

NuGet Lisenssi: MIT .NET

Ongelma: Umamin Dokumentaation aukko

Umamin API:n kanssa on vastassa tämä:

  • Ei vahvistusta syötteille Lähetä epämuodostunut GUID.
  • Kryptiset reaktiot Onko botti havaittu? "beep boop"Siinä kaikki.
  • Muutosten katkaiseminen - Eri versioiden väliset muuttujat (path vs. url, hostname vs. host)
  • Aikaleiman sekavuus Lykkyä tykö, kun selvität sen.
  • JWT-vastaukset Joskus täydet kuormat, joskus vain vierailijakortti. Ei asiakirjoja, joissa selitetään, milloin tai miksi.

Tämä sopii nopeaan prototyyppiin, mutta tuotantoon tarvitaan jotain parempaa.

Vartiolauseet: Epäonnistu nopeasti kontekstin kanssa

Pahimpia vikoja ovat ne, jotka epäonnistuvat äänettömästi. Umami.NET nappaa kokoonpanovirheitä käynnistyksessä, ennen kuin ne voivat aiheuttaa ongelmia tuotannossa.

Määritysvalidointi

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ää.

Pyyntövalidointi avuliailla ehdotuksilla

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.

Päivämäärän vaihteluvälin vahvistaminen

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 Quirksien käsittely

"Beep Boop" -ongelma

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.

Muuttujan nimi muuttuu

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"];

Aikaleimakonversiot

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.

Taustan käsittely kanavilla

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.

Kokeile uudelleen Pollyn kanssa

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ä.

Tunnistus automaattiohjauksella

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.

Testausinfrastruktuuri

Tuotantovalmiiden koodien täytyy olla kattavia testejä. Tämän rakensin:

Lokinvarmennuksen väärennös

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));
}

Custom Mock HTTP -käsittelijät

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:

  • Taustatapahtumat todella käsitellään
  • Käsittely valmistuu kohtuullisessa ajassa
  • Haavoittuneisuudesta valekäsittelijällä kerrotaan asianmukaisesti

Kattava testipeite

Testisarja kattaa:

-

  • Tapahtumaseuranta datan kanssa ja ilman
  • Sivunkatselun seuranta
  • Käyttäjän henkilöllisyys
  • JWT-vasteen purku
  • â € € € . Taustan käsittely aikalisällä
  • â € € € € uteliaisuus merkkijono sukupolvi
  • Tunnistus ja virkistäytyminen
  • â € € € Metrics ja sivukatselmukset datahaku

Tosielämän käyttö

ASP.NET Core -sovelluksen käyttö on näin yksinkertaista:

Aseta ohjelma.cs:ssä

builder.Services.SetupUmamiClient(builder.Configuration);

Kirjastossa lukee: appsettings.json:

{
  "Analytics": {
    "UmamiPath": "https://analytics.yoursite.com",
    "WebsiteId": "your-website-guid"
  }
}

Seuraa tapahtumia

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");
    }
}

Hae analyysitiedot

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");
    }
}

Mitä seuraavaksi 1,0?

Ennen 1.0-julkaisua keskityn seuraaviin aiheisiin:

  • Kokonaisvaltainen API-dokumentaatio
  • NuGet-paketin julkaisu
  • Suorituskykyä koskevat viitearvot
  • Lisämukavuusmenetelmät yhteisiä analytiikkakyselyitä varten

Päätelmät

Tuotantovalmian kirjaston rakentamisessa ei ole kyse vain API:n käärimisestä vaan siitä, että luodaan kokemus, joka on parempi Umami.NET korvaa Umamin dokumentointipuutteet:

  • Validointi, joka selittää, mikä meni pieleen ja miten se korjataan
  • Uteliaan API-käyttäytymisen miellyttävä käsittely
  • Kattava testaus, joka todistaa sen toimivan
  • Taustakäsittely, joka ei estä sovellustasi
  • Kestävä virhekäsittely automaattisilla retriiteillä

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!

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.