Back to "Tillväga 1.0: Att göra Umami | . | NET-produktion redo"

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

Tillväga 1.0: Att göra Umami | . | NET-produktion redo

Thursday, 20 November 2025

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.

  1. Den komplexa valideringen med hjälpsamma felvaror
  2. Robust testinfrastruktur
  3. Ganska bra handling av fel för riktiga scenarior

Låt mig gå igenom vad som gör den här bibliotekets produktion

NuGet License: MIT .NET

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 path mot url, hostname mot host)
  • 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

logo

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