# Naar 1.0: Umami.NET productieklaar maken

<!-- category -- C#, Umami, Analytics, Open Source -->
<datetime class="hidden">2025-11-20T14:30</datetime>

## Inleiding

Toen ik voor het eerst integreerde [Umami-analyse](https://umami.is/) in mijn blog platform, Ik liep snel in een frustrerende realiteit: Umami's API documentatie is... laten we liefdadigheid en noem het "minimaal." Foutmeldingen zijn cryptisch op zijn best, niet-bestaand op het slechtst. Parameters veranderen tussen versies zonder waarschuwing. En laat me niet eens beginnen op de "piep boop" bot detectie reactie.

Dus ik bouwde [Umami.NET](https://github.com/scottgal/mostlylucidweb/tree/main/Umami.Net) - niet alleen als een eenvoudige HTTP wrapper, maar als een productie-ready client library die compenseert voor al Umami's eigenaardigheden. Aangezien ik heb gewerkt aan een 1.0 release, ik heb me gericht op drie kritieke gebieden:

1. **Uitgebreide validatie met nuttige foutmeldingen**
2. **Robuuste testinfrastructuur**
3. **Graceful error handling voor real-world scenario's**

Laat me je vertellen wat deze bibliotheek productie-klaar maakt.

[![NuGet](https://img.shields.io/nuget/v/Umami.Net.svg?style=flat-square)](https://www.nuget.org/packages/Umami.Net/)
[![Licentie: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
[![.NET](https://img.shields.io/badge/.NET-9.0-purple?style=flat-square)](https://dotnet.microsoft.com/)

[TOC]

## Het probleem: Umami's Documentatie Gap

Dit is waar je mee te maken hebt als je rechtstreeks met Umami's API werkt:

- **Geen invoervalidatie** Een misvormde Guid sturen?
- **Cryptische reacties** - Bot gedetecteerd? `"beep boop"`Dat is het.
- **Veranderingen breken** - Parameters hernoemd tussen versies (`path` vs `url`, `hostname` vs `host`)
- **Tijdstempelverwarring** Succes met het uitzoeken.
- **JWT-responsen** - Soms volle lading, soms gewoon een bezoeker ID... geen documentatie die uitlegt wanneer of waarom.

Dit is prima voor een snel prototype, maar voor de productie? Je hebt iets beters nodig.

## Bewakersclausules: faalt snel met context

De ergste bugs zijn degenen die stil falen. Umami.NET vangt configuratiefouten bij het opstarten voordat ze problemen in de productie kunnen veroorzaken.

### Configuratievalidatie

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

Dit loopt bij het opstarten in uw `Program.cs`. Als uw configuratie fout is, weet u het direct - niet wanneer de eerste analytics event probeert te verzenden.

### Validatie aanvragen met behulp van nuttige suggesties

Maar de echte magie zit in de query string helper. Bekijk deze foutmeldingen:

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

Nota van de `Suggestion:` Voorvoegsel? Elke foutmelding vertelt u **Wat is er misgegaan?** en **hoe het te repareren**. Dit is de documentatie die Umami zou moeten hebben verstrekt.

### Datumbereikvalidatie

Bij het bouwen van analytische queries, kunnen datumbereiken lastig zijn. De bibliotheek vangt deze fouten voor u op:

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

## Omgaan met Umami's Quirks

### Het "Beep Boop" probleem

Umami's bot detectie geeft een platte tekst reactie terug: `"beep boop"`Niet JSON, geen echte statuscode.

Dit is hoe Umami.NET ermee omgaat:

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

Uw code krijgt een schoon enum:

```csharp
public enum ResponseStatus
{
    Failed,
    BotDetected,
    Success
}
```

Geen rare antwoorden meer ontleden - controleer gewoon de status.

### Parameternaamwijzigingen

Umami hernoemde parameters tussen API-versies. Hebben ze dit gedocumenteerd? Natuurlijk niet. De bibliotheek behandelt beide:

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

### Tijdstempelconversies

Umami gebruikt Unix milliseconden voor tijdstempels. Hier is een helper die het pijnloos maakt:

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

Nu kun je met normaal werken. `DateTime` objecten en laat de bibliotheek de conversie afhandelen.

## Achtergrondverwerking met kanalen

Analytics mag uw toepassing nooit blokkeren. Umami.NET bevat een achtergrondzender die gebruik maakt van `System.Threading.Channels`:

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

Gebeurtenissen worden in de wachtrij in het geheugen geplaatst en asynchroon verwerkt. Uw webverzoeken keren onmiddellijk terug, analytics gebeuren op de achtergrond.

## Beleid opnieuw proberen met Polly

Netwerkstoringen gebeuren. De bibliotheek gebruikt Polly voor veerkrachtige HTTP-oproepen:

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

Voorbijgaande storingen en 503 fouten leiden tot automatische herhalingen met exponentiële backoff. Uw analyses zijn veerkrachtig voor tijdelijke netwerkproblemen.

## Authenticatie met automatisch opnieuw proberen

Bij het ophalen van analytics-gegevens (niet alleen versturen van evenementen), hebt u authenticatie nodig. De bibliotheek behandelt het verlopen van token automatisch:

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

Je hoeft nooit na te denken over token management - het werkt gewoon.

## Testinfrastructuur

Productie-ready code moet uitgebreide tests. Hier is wat ik gebouwd:

### FakeLogger voor logverificatie

Microsoft's gebruiken `FakeLogger` pakket, testen kunnen loggedrag verifiëren:

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

### Aangepaste Mock HTTP-handlers

Het testen van async operaties is lastig. Hier is een patroon met behulp van `TaskCompletionSource`:

```csharp
[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
}
```

Dit patroon zorgt voor:

- Achtergrondgebeurtenissen worden daadwerkelijk verwerkt
- De verwerking is binnen een redelijke termijn voltooid
- Asserties in de mock handler zijn correct gemeld

### Uitgebreide testdekking

De test suite heeft betrekking op:

- Configuratievalidatie (ongeldige GUID's, ontbrekende URL's)
- Gebeurtenis volgen met en zonder gegevens
- Paginaweergave-tracking
- Gebruikersidentificatie
- Botdetectie-afhandeling
- Decodering van de JWT-respons
- Achtergrondbewerking met time-outs
- Datumbereikvalidatie
- Verlangen string generatie
- Authenticatie en token vernieuwen
- Metrics and pageviews data retrieval

## Gebruik in de reële wereld

Hier is hoe eenvoudig het is om te gebruiken in een ASP.NET Core applicatie:

### Instellen in Programma.cs

```csharp
builder.Services.SetupUmamiClient(builder.Configuration);
```

Dat is het, de bibliotheek leest jouw `appsettings.json`:

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

### Track-gebeurtenissen

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

### Analytics-gegevens ophalen

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

## Wat is het volgende voor 1.0?

De bibliotheek is feature-complete en battle-getest in productie op deze zeer blog. Voor de 1.0 release, ik ben gericht op:

- Uitgebreide API-documentatie
- NuGet package publishing
- Prestatie-benchmarks
- Extra gemaksmethoden voor gemeenschappelijke analysevragen

## Conclusie

Het bouwen van een productie-ready bibliotheek gaat niet alleen over het inpakken van een API - het gaat over het creëren van een ervaring die's **beter** dan de API rechtstreeks te gebruiken. Umami.NET compenseert Umami's documentatie hiaten met:

- **Validatie dat verklaart wat er mis ging en hoe het te repareren**
- **Graceful handling van eigenzinnige API gedrag**
- **Uitgebreide tests die bewijzen dat het werkt**
- **Achtergrondverwerking die uw app niet blokkeert**
- **Herstellende foutafhandeling met automatische herhalingen**

Als je Umami analytics gebruikt in een .NET applicatie, zou ik graag willen dat je het probeert. [Umami.NET](https://github.com/scottgal/mostlylucidweb/tree/main/Umami.Net)Het is open source, zwaar getest, en ontworpen om je leven gemakkelijker te maken.

Heb je vragen of suggesties? Open een probleem op GitHub of reik uit in de reacties hieronder!