# Προς 1.0: Κάνοντας την παραγωγή Umami.NET έτοιμη

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

## Εισαγωγή

Όταν πρωτοεμφανίστηκα [Umami analytics](https://umami.is/) στην πλατφόρμα blog μου, έτρεξα γρήγορα σε μια απογοητευτική πραγματικότητα: η τεκμηρίωση API του Umami είναι... ας είναι φιλανθρωπικό και να το αποκαλούμε "μίνιμαλ." Τα μηνύματα σφάλματος είναι αινιγματικά, ανύπαρκτα στη χειρότερη περίπτωση. Οι παράμετροι αλλάζουν μεταξύ των εκδόσεων χωρίς προειδοποίηση.

Έτσι έφτιαξα [Umami.NET](https://github.com/scottgal/mostlylucidweb/tree/main/Umami.Net) - όχι μόνο ως ένα απλό περιτύλιγμα HTTP, αλλά ως μια έτοιμη για την παραγωγή βιβλιοθήκη πελατών που αντισταθμίζει όλες τις ιδιοτροπίες του Umami. Καθώς έχω εργαστεί για μια έκδοση 1.0, έχω επικεντρωθεί σε τρεις κρίσιμους τομείς:

1. **Πλήρης επικύρωση με χρήσιμα μηνύματα σφάλματος**
2. **Ανθεκτική υποδομή δοκιμών**
3. **Χειρισμός χαριτωμένων λαθών για σενάρια πραγματικού κόσμου**

Άσε με να σου εξηγήσω τι κάνει αυτή τη βιβλιοθήκη έτοιμη για παραγωγή.

[![NuGetCity name (optional, probably does not need a translation)](https://img.shields.io/nuget/v/Umami.Net.svg?style=flat-square)](https://www.nuget.org/packages/Umami.Net/)
[![Άδεια: 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]

## Το πρόβλημα: Umami's Documentation Gap

Να τι αντιμετωπίζετε όταν δουλεύετε με το API του Umami απευθείας:

- **Δεν υπάρχει επικύρωση εισόδου** - Στέλνεις κακοσχηματισμένο GUID;
- **Κρυπτικές αντιδράσεις** - Το ρομπότ ανιχνεύτηκε; `"beep boop"`- Αυτό είναι.
- **Σπάζοντας τις αλλαγές** - Οι παράμετροι μετονομάστηκαν μεταξύ των εκδόσεων (`path` vs `url`, `hostname` vs `host`)
- **Συγχώνευση χρονοσφραγίδας** Καλή τύχη στο να το βρεις.
- **Απαντήσεις JWT** - Μερικές φορές γεμάτα φορτία, μερικές φορές μόνο μια ταυτότητα επισκέπτη.

Αυτό είναι καλό για ένα γρήγορο πρωτότυπο, αλλά για την παραγωγή; Χρειάζεσαι κάτι καλύτερο.

## Προφυλάξεις: Αποτύχετε γρήγορα με το πλαίσιο

Τα χειρότερα σφάλματα είναι αυτά που αποτυγχάνουν σιωπηλά. Umami.NET πιάνει λάθη διαμόρφωσης κατά την εκκίνηση πριν μπορούν να προκαλέσουν προβλήματα στην παραγωγή.

### Επικύρωση ρύθμισης

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

Αυτό τρέχει κατά την εκκίνηση σας `Program.cs`. Εάν η διαμόρφωση σας είναι λάθος, ξέρετε αμέσως - όχι όταν το πρώτο γεγονός αναλυτικής προσπαθεί να στείλει.

### Ζητήστε Επιβεβαίωση με Υποβοηθητικές Προτάσεις

Αλλά η πραγματική μαγεία είναι στην ερώτηση string helper. Δείτε αυτά τα μηνύματα σφάλματος:

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

Παρατηρήστε το `Suggestion:` Πρόθεμα; Κάθε μήνυμα λάθους σας λέει **Τι πήγε στραβά;** και **πώς να το διορθώσετε**Αυτή είναι η τεκμηρίωση που θα έπρεπε να έχει δώσει ο Umami.

### Ημερομηνία επικύρωσης εύρους

Όταν χτίζει ερωτήματα αναλυτικής, η κλίμακα των ημερομηνιών μπορεί να είναι δύσκολη. Η βιβλιοθήκη πιάνει αυτά τα λάθη για σας:

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

## Χειρισμός των Κουίρκς του Ουμάμι

### Το πρόβλημα "Beep Boop"

Η ανίχνευση ρομπότ του Umami επιστρέφει μια απλή απάντηση κειμένου: `"beep boop"`Όχι ο Τζέισον, δεν είναι σωστός κωδικός.

Να πώς το χειρίζεται το Umami.NET:

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

Ο κώδικάς σας παίρνει ένα καθαρό enum:

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

Τέρμα οι αλλόκοτες απαντήσεις, απλά έλεγξε την κατάσταση.

### Αλλαγές ονόματος παραμέτρου

Umami μετονομάστηκε παραμέτρους μεταξύ API εκδόσεις. Καταγράφετε αυτό; Φυσικά όχι. Η βιβλιοθήκη χειρίζεται και τα δύο:

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

### Μετατροπές χρονοσφραγίσεων

Ο Umami χρησιμοποιεί το Unix milliseconds για χρονοσφραγίσεις.

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

Τώρα μπορείς να δουλέψεις με το φυσιολογικό. `DateTime` αντικείμενα και αφήστε τη βιβλιοθήκη να χειριστεί τη μετατροπή.

## Επεξεργασία φόντου με Κανάλια

Η analytics δεν πρέπει ποτέ να εμποδίζει την εφαρμογή σας. Το Umami.NET περιλαμβάνει έναν αποστολέα υποβάθρου που χρησιμοποιεί `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");
            }
        }
    }
}
```

Τα γεγονότα βρίσκονται σε ουρά στη μνήμη και υποβάλλονται σε ασύγχρονη επεξεργασία. Τα αιτήματά σας στο διαδίκτυο επιστρέφουν αμέσως, η ανάλυση γίνεται στο παρασκήνιο.

## Επαναπροσπαθήστε Πολιτικές με την Πόλι

Η βιβλιοθήκη χρησιμοποιεί την Polly για ανθεκτικές κλήσεις HTTP:

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

Παροδικές αποτυχίες και 503 σφάλματα ενεργοποιούν αυτόματες επαναλήψεις με εκθετική backoff. Τα αναλυτικά σας είναι ανθεκτικά σε προσωρινά ζητήματα δικτύου.

## Auto-Retry

Όταν φέρνετε δεδομένα αναλυτικής (όχι μόνο αποστολή γεγονότων), χρειάζεστε εξακρίβωση ταυτότητας. Η βιβλιοθήκη χειρίζεται τη συμβολική λήξη αυτόματα:

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

Δεν χρειάζεται να σκέφτεσαι ποτέ τη διαχείριση των σημάτων - απλά δουλεύει.

## Υποδομές δοκιμών

Κοίτα τι έφτιαξα:

### Ψεύτικος λογότυπος για επαλήθευση καταγραφής

Χρήση της Microsoft `FakeLogger` πακέτο, δοκιμές μπορούν να επαληθεύσουν τη συμπεριφορά καταγραφής:

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

### Χειριστές συνήθειας Mock HTTP

Η δοκιμή ασύγχρονων λειτουργιών είναι δύσκολη. `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
}
```

Αυτό το μοτίβο εξασφαλίζει:

- Τα γεγονότα υποβάθρου στην πραγματικότητα υποβάλλονται σε επεξεργασία
- Η επεξεργασία ολοκληρώνεται εντός εύλογου χρονικού διαστήματος
- Οι παρεμβολές στο mockee handler αναφέρονται σωστά

### Συνολική κάλυψη δοκιμών

Η δοκιμαστική σουίτα καλύπτει:

- Επικύρωση ρύθμισης (μη έγκυρο GUIDs, λείπει URL)
- Εντοπισμός γεγονότων με και χωρίς δεδομένα
- Εντοπισμός προβολής σελίδας
- Ταυτότητα χρήστη
- Χειρισμός ανίχνευσης βότκας
- Αποκωδικοποίηση απάντησης JWT
- □ Επεξεργασία υποβάθρου με timeouts
- Επικύρωση εύρους ημερομηνίας
- Γενιά σειράς ερωτημάτων
- Αυθεντικότητα και αναζωογονητική ένδειξη
- Μετρική και pageviews ανάκτηση δεδομένων

## Πραγματική Παγκόσμια Χρήση

Εδώ είναι πόσο απλό είναι να το χρησιμοποιήσετε σε μια εφαρμογή ASP.NET Core:

### Ρύθμιση στο πρόγραμμα.cs

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

Η βιβλιοθήκη διαβάζει το δικό σου. `appsettings.json`:

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

### Εκδηλώσεις κομματιού

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

### Φέρτε τα δεδομένα αναλυτικής ανάλυσης

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

## Ποιο είναι το επόμενο για 1.0;

Η βιβλιοθήκη είναι πλήρης και δοκιμασμένη στην παραγωγή σε αυτό ακριβώς το blog. Πριν από την έκδοση 1.0, επικεντρώνομαι σε:

- Περιεκτική τεκμηρίωση API
- □ NuGet package publishing
- Οι δείκτες αναφοράς επιδόσεων του συστήματος αναφοράς επιδόσεων είναι οι εξής:
- Επιπρόσθετες μέθοδοι ευκολίας για κοινά ερωτήματα ανάλυσης

## Συμπέρασμα

Η οικοδόμηση μιας βιβλιοθήκης έτοιμη για την παραγωγή δεν είναι μόνο για την περιτύλιξη ενός API - πρόκειται για τη δημιουργία μιας εμπειρίας που είναι **Καλύτερα.** Το Umami.NET αντισταθμίζει τα κενά τεκμηρίωσης του Umami με:

- **Επιβεβαίωση που εξηγεί τι πήγε στραβά και πώς να το διορθώσει**
- **Ευγενικός χειρισμός ιδιότροπων συμπεριφορών API**
- **Πλήρης δοκιμή που αποδεικνύει ότι λειτουργεί**
- **Επεξεργασία ιστορικού που δεν εμποδίζει την εφαρμογή σας**
- **Ανθεκτικός χειρισμός λάθους με αυτόματες επαναλήψεις**

Αν χρησιμοποιείτε το Umami analytics σε εφαρμογή .NET, θα ήθελα πολύ να δοκιμάσετε [Umami.NET](https://github.com/scottgal/mostlylucidweb/tree/main/Umami.Net)Είναι ανοιχτή πηγή, έχει δοκιμαστεί πολύ, και έχει σχεδιαστεί για να κάνει τη ζωή σας ευκολότερη.

Έχετε ερωτήσεις ή προτάσεις; Ανοίξτε ένα θέμα για το GitHub ή επικοινωνήστε με τα παρακάτω σχόλια!