Back to "Mot 1.0: Gör Umami.NET-produktionen klar"

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

Mot 1.0: Gör Umami.NET-produktionen klar

Thursday, 20 November 2025

Inledning

När jag först integrerade Umamianalys in i min blogg plattform, sprang jag snabbt in i en frustrerande verklighet: Umamis API dokumentation är ... låt oss vara välgörande och kalla det "minimal." Felmeddelanden är kryptiska i bästa fall, obefintlig i värsta fall. Parametrar ändras mellan versioner utan varning. Och inte ens få mig att starta på "beep boop" bot detektion svar.

Så jag byggde Ummami.NET Ordförande - inte bara som en enkel HTTP omslag, men som en produktionsklar klient bibliotek som kompenserar för alla Umami s egenheter. När jag har arbetat för en 1.0 release, har jag fokuserat på tre kritiska områden:

  1. Omfattande validering med användbara felmeddelanden
  2. Robust testinfrastruktur
  3. Graceful felhantering för verkliga scenarier

Låt mig gå igenom vad som gör biblioteket färdigt.

Hämta Licens: MIT .NET (netto)

Problemet: Umamis dokumentationsklyfta

Här är vad du står inför när du arbetar med Umamis API direkt:

  • Ingen validering av indata - Skicka ett missbildat GUID?
  • Kryptiska svar - Bot upptäckt? "beep boop"Det är allt.
  • Bryta ändringar - Parametrar som bytt namn mellan versionerna (path vs url, hostname vs host)
  • Tidsstämpel förvirring Lycka till med att lista ut det.
  • Svar från JWT - Ibland fulla laster, ibland bara ett besöks- ID. Ingen dokumentation som förklarar när eller varför.

Det går bra för en snabb prototyp, men för produktionen?

Vaktklausuler: Misslyckas snabbt med sammanhang

De värsta felen är de som misslyckas tyst. Umami.NET fångar konfigurationsfel vid start innan de kan orsaka problem i produktionen.

Inställningsvalidering

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

Detta körs vid start i din Program.cs. Om din konfiguration är fel, du vet omedelbart - inte när den första analys händelse försöker skicka.

Begär validering med hjälpsamma förslag

Men den verkliga magin finns i frågesträngshjälparen. Kolla in dessa felmeddelanden:

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

Lägg märke till Suggestion: Prefix? Varje felmeddelande talar om för dig vad som gick fel och hur man fixar det. – Detta är den dokumentation Umami borde ha lämnat.

Datumområdesvalidering

När du bygger analysfrågor, datumintervall kan vara svårt. Biblioteket fångar dessa misstag för dig:

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

Ta hand om Umamis frågesporter

Problemet med "pipboppen"

Umamis bot upptäckt returnerar ett enkelt textsvar: "beep boop"Inte JSON, ingen riktig statuskod.

Så här hanterar Umami.NET 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
}

Inga fler tolkning konstiga svar - bara kolla status.

Parameternamnändringar

Umami döpte om parametrar mellan API-versioner. Dokumenterade de detta? Naturligtvis inte. Biblioteket hanterar båda:

// Support both old and new parameter names
request.Path = queryParams["path"] ?? queryParams["url"];
request.Hostname = queryParams["hostname"] ?? queryParams["host"];

Tidsstämpelkonverteringar

Umami använder Unix millisekunder för tidsstämpel. Här är en hjälpare som gör det smärtfritt:

public static long ToMilliseconds(this DateTime dateTime)
{
    var dateTimeOffset = new DateTimeOffset(dateTime.ToUniversalTime());
    return dateTimeOffset.ToUnixTimeMilliseconds();
}

Nu kan du jobba med det normala. DateTime objekt och låta biblioteket hantera konverteringen.

Bakgrundsbehandling med kanaler

Analys ska aldrig blockera din ansökan. Umami.NET innehåller en bakgrundsavsä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");
            }
        }
    }
}

Händelser köas i minnet och behandlas asynkront. Dina webbförfrågningar returneras omedelbart, analys sker i bakgrunden.

Försök politik med Polly

Nätverksfel inträffar. Biblioteket använder Polly för motståndskraftiga HTTP-samtal:

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

Övergående fel och 503 fel utlöser automatiska retries med exponentiell backoff. Din analys är motståndskraftig mot tillfälliga nätverksproblem.

Behörighetskontroll med Auto-Retry

När du hämtar analysdata (inte bara skicka händelser) behöver du behörighetskontroll. Biblioteket hanterar automatiskt token extension:

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å token management - det fungerar bara.

Provningsinfrastruktur

Det här är vad jag byggde:

FakeLogger för loggverifiering

Använda Microsoft's FakeLogger paket, tester kan verifiera loggning beteende:

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

Anpassade HTTP-hanterare i Mock

Att testa async-operationer är svårt. Här är ett mönster med hjälp av 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 säkerställer:

  • Bakgrund händelser faktiskt behandlas
  • Bearbetning fullföljs inom rimlig tid
  • Assertioner i mock-handlern rapporteras korrekt

Omfattande provtäckning

Testsviten omfattar följande:

  • på inställningsvalidering (ogiltiga GUID, saknade webbadresser)
  • Händelsespårning med och utan data
  • Sidvysspårning
  • på användaridentifikation
  • Hantering av Bot-detektion
  • Avkodning av JWT-svar
  • på bakgrundsbearbetning med timeouts
  • Validering av datumintervall
  • på frågesträngsgenerering
  • på autentisering och token uppdatering
  • på Metrics och sidvyer datahämtning

Real-World användning

Så här enkelt är det att använda i en ASP.NET Core-applikation:

Ställ in i program.cs

builder.Services.SetupUmamiClient(builder.Configuration);

Biblioteket läser din bok. appsettings.json:

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

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

Hämta analysdata

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 för 1.0?

Biblioteket är funktion-komplett och strids-testad i produktionen på just denna blogg. Innan 1.0 release, Jag fokuserar på:

  • till Omfattande API-dokumentation
  • på NuGet paketpublicering
  • Uppfyllanderiktmärken
  • på ytterligare bekvämlighetsmetoder för vanliga analysfrågor

Slutsatser

Att bygga ett produktionsklart bibliotek handlar inte bara om att slå in ett API - det handlar om att skapa en upplevelse som är bättre än att använda API:et direkt. Umami.NET kompenserar för Umamis dokumentationsluckor med:

  • Validering som förklarar vad som gick fel och hur man fixar det
  • Förtjusande hantering av udda API-beteenden
  • Omfattande tester som bevisar att det fungerar
  • Bakgrundsbehandling som inte blockerar din app
  • Motståndskraftig felhantering med automatiska retries

Om du använder Umami analytics i en .NET-applikation, skulle jag gärna vilja att du provar Ummami.NET Ordförande. Det är öppen källkod, kraftigt testad, och utformad för att göra ditt liv lättare.

Har du frågor eller förslag? Öppna ett nummer på GitHub eller kontakta oss i kommentarerna nedan!

logo

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