# До 1. 0: Виробництво амамі. NET готове

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

## Вступ

Коли я вперше інтегрований [Аналітичні аналітики у Умамі](https://umami.is/) У моїй платформі блогу я швидко натрапив на розчаруючу реальність: документація з API Амамі... давайте будемо благодійними і назвемо її "мініальними." Повідомлення про помилки у найкращому випадку закриті, невиявлені у найгіршому. Параметри змінюються між версіями без попередження. І навіть не дайте мені розпочати з відповіді на визначення "beep butp" bot.

Отже, я побудував [Umami. NET](https://github.com/scottgal/mostlylucidweb/tree/main/Umami.Net) - не просто як проста обгортка HTTP, а як виробна клієнтська бібліотека, яка компенсує всі примхи Умамі. Оскільки я працювала над випуском версії 1. 0, я зосередилася на трьох важливих ділянках:

1. **Докладна перевірка на корисні повідомлення про помилки**
2. **Випробовування інфраструктури**
3. **Грандіозна обробка помилок для сценаріїв реального світу**

Дозвольте мені провести вас через те, що робить цю бібліотеку готовою до виробництва.

[![NuGet](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]

## Проблема. Прірва від документації Умамі.

Ось проти чого вам доводиться працювати з API Умамі прямо:

- **Без перевірки вхідних даних** - Відправте погано сформований GUID?
- **Криптичні відповіді** - Бот виявлено? `"beep boop"`Ось так.
- **Breaking changes** - Параметри перейменовані між версіями (`path` всunit description in lists `url`, `hostname` всunit description in lists `host`)
- **Часова плутанина** - О мілісекунди Unix?
- **Відповіді JWT** Подеколи повно вантажів, іноді лише ідентифікаційне посвідчення.

Це добре для швидкого прототипу, але для виробництва?

## Охорона Клаусів: Швидка помилка з контекстом

Найгірші вади - це ті, які безшумно зазнають невдачі. Uami. 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`. Якщо ваші налаштування неправильні, ви знаєте негайно - не тоді, коли перші аналітичні події намагаються надіслати.

### Запит щодо підтвердження за допомогою корисних порад

Але справжня магія знаходиться у допоміжному рядку запиту. Перевірте такі повідомлення про помилки:

```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:` префікс? Кожне повідомлення про помилку повідомляє вам **while failed width** і **як це виправити**Це документація, яку мав надати Умамі.

### Перевірка діапазону дат

Під час створення аналітичних запитів діапазон дат може бути дуже складним. Бібліотека знайде для вас такі помилки:

```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"

Виявлення bott umami повертає звичайну текстову відповідь: `"beep boop"`Не JSON, не є коректним кодексом статусу.

Ось як цим займається Умамі. НЕТ:

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

Ваш код получил чистый перелік:

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

Більше ніякого аналізу дивних відповідей - просто перевірте статус.

### Зміна назв параметрів

Параметри amami перейменовано між версіями API. Чи документували вони це? Звичайно ж, що ні. Бібліотека керує обома версіями:

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

### Перетворення часових штампів

Umami використовує мілісекунди Unix для часових штампів. Ось помічник, який робить його безболісно:

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

Тепер ви можете працювати з нормальним `DateTime` об' єкти і дозволити бібліотеці керувати перетворенням.

## Обробка тла за допомогою каналів

Аналітичні дані ніколи не повинні блокувати вашу програму. Уамі. 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 помилки запускають автоматичні зворотні з' єднання з експоненціальними ресурсами. Ваші аналітичні дії є стійкими до тимчасових проблем у мережі.

## Розпізнавання за допомогою автоповторення

Під час отримання аналітичних даних (не лише надсилання подій), вам слід автентифікуватися. Бібліотека автоматично виконує строк дії ключа:

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

Ви ніколи не повинні думати про управління ключами - це просто працює.

## Перевірка інфраструктури

Виготовлений-готовий код потребує комплексних тестів. Ось що я побудував:

### FictLogger для перевірки журналу

Користування 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
}
```

Цей шаблон забезпечує:

- Події тла насправді оброблено
- Обробка завершується протягом вказаного часу
- Умовні оголошення у імітатора правильно повідомлені.

### Пробний обкладинок

Комплекс тестів покриває:

- Підтвердження налаштувань (невиправним GUIDs, відсутні адреси URL)
- ужі- д/ д відстежуються з даними і без них.
- Стеження за сторінкою
- }Імедієнтація
- Програма для виявлення опрацювання
- Декодування відповіді JWT
- ⇩ Верхня обробка тайм- ауту
- Перевірка діапазону дат
- Приблизно стільки ж, скільки і раніше.
- ⇩ Розпізнавання і освіження ключа
- ⇩ Метриці і перегляд сторінок отримують дані

## Використання у реальному світі

Ось як просто користуватися у програмі ASP. NET:

### Налаштування у програмі. 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?

Бібліотеку буде перевірено як можливості, так і під час битви у виробництві на цьому блозі. Перед випуском 1. 0 я фокусуюся на:

- Документація з API comeve
- Публікація пакунків ⇩GOM
- ведьмиunit synonyms for matching user input
- біса Додаткові методи зручності для звичайних аналітичних запитів

## Висновки

Побудова бібліотеки, готової до виробництва, - це не просто обгортка API, а створення досвіду. **краще** ніж безпосередньо користуватися API. Amami. NET компенсує пропуски документації Умамі:

- **Перевірка, що пояснює, що пішло не так і як це виправити**
- **Милосерде ставлення до дивної поведінки API**
- **Гідне випробування, яке доводить, що воно діє.**
- **Обробка тла, яка не блокує вашу програму**
- **Змінна обробка помилок за допомогою автоматичних повторень**

Якщо ви використовуєте аналітичні аналітичні засоби .NET, я хотів би, щоб ви спробували [Umami. NET](https://github.com/scottgal/mostlylucidweb/tree/main/Umami.Net)Він відкритий, ретельно перевірений і спроектований, щоб полегшити ваше життя.

Питання або пропозиції: розгорніть GitHub або запишіть нижченаведені коментарі!