Verso 1.0: Rendere pronta la produzione di Umami.NET (Italiano (Italian))

Verso 1.0: Rendere pronta la produzione di Umami.NET

Thursday, 20 November 2025

//

10 minute read

Introduzione

Quando ho integrato per la prima volta Analisi Umami nella mia piattaforma di blog, ho incontrato rapidamente una realtà frustrante: la documentazione API di Umami è... siamo caritatevoli e chiamiamolo "minimale." I messaggi di errore sono criptici nel migliore dei casi, inesistenti nel peggiore. I parametri cambiano tra le versioni senza preavviso. E non fatemi nemmeno iniziare la risposta di rilevamento bot "beep boop."

Cosi' ho costruito Umami.NET - non solo come semplice wrapper HTTP, ma come una libreria client pronta alla produzione che compensa tutte le stranezze di Umami. Mentre sto lavorando a una release 1.0, mi sono concentrato su tre aree critiche:

  1. Convalida completa con utili messaggi di errore
  2. Infrastrutture di collaudo robuste
  3. Gestione di errori graziosi per scenari del mondo reale

Lascia che ti spieghi cosa rende la produzione di questa biblioteca pronta.

NuGetCity name (optional, probably does not need a translation) Licenza: MIT .NET

Il problema: Umami's Documentation Gap

Ecco cosa stai affrontando quando lavori direttamente con l'API di Umami:

  • Nessuna convalida degli input - Inviare una GUID malformata?
  • Risposte criptiche - Bot rilevato? "beep boop"E' tutto.
  • Breaking changes - Parametri rinominati tra le versioni (path vs url, hostname vs host)
  • Confusione del timestamp - Unix millisecondi?
  • Risposte JWT - A volte carichi pieni, a volte solo un ID del visitatore. Nessuna documentazione che spieghi quando o perché.

Questo va bene per un prototipo veloce, ma per la produzione? Hai bisogno di qualcosa di meglio.

Clausole di guardia: Fail Fast con il contesto

I bug peggiori sono quelli che non riescono silenziosamente. Umami.NET cattura gli errori di configurazione all'avvio prima che possano causare problemi nella produzione.

Configurazione Validation

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

Questo funziona all'avvio nel vostro Program.cs. Se la configurazione è errata, si sa immediatamente - non quando il primo evento di analisi tenta di inviare.

Richiedi la convalida con suggerimenti utili

Ma la vera magia è nella stringa di ricerca helper. Controlla questi messaggi di errore:

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

Notare la Suggestion: prefisso? Ogni messaggio di errore ti dice ciò che è andato storto e come ripararloQuesta è la documentazione che Umami avrebbe dovuto fornire.

Convalida dell'intervallo di date

Quando si costruiscono query di analisi, gli intervalli di date possono essere difficili. La libreria cattura questi errori per voi:

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

Gestire gli Umami's Quirks

Il problema "Beep Boop"

Il rilevamento dei bot di Umami restituisce una risposta testuale: "beep boop"Non e' JSON, non e' un vero e proprio codice di stato, solo... beep boop.

Ecco come lo gestisce Umami.NET:

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

Il tuo codice ottiene un enum pulito:

public enum ResponseStatus
{
    Failed,
    BotDetected,
    Success
}

Basta analizzare le risposte strane - basta controllare lo stato.

Cambiamenti del nome del parametro

Umami ha rinominato i parametri tra le versioni API. L'hanno documentato? Certo che no. La libreria gestisce entrambi:

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

Conversioni timestamp

Umami usa unix millisecondi come timestamp. Ecco un aiutante che lo rende indolore:

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

Ora si può lavorare con normale DateTime oggetti e lasciare la libreria gestire la conversione.

Elaborazione dello sfondo con i canali

Analytics non dovrebbe mai bloccare l'applicazione. Umami.NET include un mittente di sfondo utilizzando 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");
            }
        }
    }
}

Gli eventi sono in coda nella memoria ed elaborati in modo asincrono. Le vostre richieste web ritornano istantaneamente, l'analisi avviene in background.

Riprova le politiche con Polly

I guasti di rete accadono. La libreria utilizza Polly per le chiamate HTTP resilienti:

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

I guasti transitori e gli errori 503 attivano le riletture automatiche con backoff esponenziale. Le tue analisi sono resistenti a problemi di rete temporanei.

Autenticazione con auto-prova

Quando si raccolgono i dati analitici (non solo l'invio di eventi), è necessaria l'autenticazione. La libreria gestisce automaticamente la scadenza del token:

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

Non devi mai pensare alla gestione dei gettoni. Funziona e basta.

Infrastruttura di prova

Il codice pronto per la produzione ha bisogno di test completi. Ecco cosa ho costruito:

FakeLogger per la verifica dei log

Uso di Microsoft FakeLogger pacchetto, i test possono verificare il comportamento di registrazione:

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

Gestori personalizzati Mock HTTP

Testare le operazioni asincrone è difficile. Ecco uno schema che utilizza 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
}

Questo modello assicura:

  • Gli eventi di sfondo sono effettivamente elaborati
  • Il trattamento si completa entro un periodo di tempo ragionevole
  • Le asserzioni nell'handler simulato sono segnalate correttamente

Copertura completa dei test

La suite di prova copre:

  • Convalida della configurazione (GUID non validi, URL mancanti)
  • Tracciamento eventi con e senza dati
  • Tracciamento vista pagina
  • Identificazione dell'utente
  • Gestione del rilevamento di bot
  • Decodifica della risposta JWT
  • Il trattamento di sfondo con timeout
  • Convalida dell'intervallo di date
  • Generazione stringa di interrogazione
  • Autenticazione e refresh token
  • Metrics and pageviews data recovery

Uso del mondo reale

Ecco come è semplice da usare in un'applicazione ASP.NET Core:

Impostazioni in Program.cs

builder.Services.SetupUmamiClient(builder.Configuration);

La biblioteca legge la tua appsettings.json:

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

Traccia eventi

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

Raccogli dati di analisi

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

Cosa c'e' dopo per 1.0?

La libreria è completa e testata in produzione proprio su questo blog. Prima della release 1.0, mi sto concentrando su:

  • Documentazione API completa
  • Pubblicazione di pacchetti NuGet
  • Performance benchmarks
  • Ulteriori metodi di convenienza per le query di analisi comuni

Conclusione

Costruire una libreria pronta per la produzione non significa solo avvolgere un'API, ma anche creare un'esperienza meglio rispetto all'utilizzo diretto dell'API. Umami.NET compensa le lacune di documentazione di Umami con:

  • Convalida che spiega cosa è andato storto e come risolverlo
  • Gestione graziosa dei comportamenti bizzarri delle API
  • Test completo che prova che funziona
  • Elaborazione dello sfondo che non blocca l'app
  • Gestione degli errori resilienti con ripetizioni automatiche

Se stai usando Umami Analytics in un'applicazione .NET, mi piacerebbe che provassi Umami.NET. E 'open source, fortemente testato, e progettato per rendere la vostra vita più facile.

Hai domande o suggerimenti? Apri un numero su GitHub o contattaci nei commenti qui sotto!

Finding related posts...
logo

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