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

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

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

Thursday, 20 November 2025

Вступ

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

Отже, я побудував Umami. NET - не просто як проста обгортка HTTP, а як виробна клієнтська бібліотека, яка компенсує всі примхи Умамі. Оскільки я працювала над випуском версії 1. 0, я зосередилася на трьох важливих ділянках:

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

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

NuGet Ліцензія: MIT . NET

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

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

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

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

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

Найгірші вади - це ті, які безшумно зазнають невдачі. Uami. NET ловить помилки налаштування під час запуску, перш ніж вони можуть спричинити проблеми у виробництві.

Перевірка налаштувань

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

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

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

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 і як це виправитиЦе документація, яку мав надати Умамі.

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

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

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, не є коректним кодексом статусу.

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

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

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

public enum ResponseStatus
{
    Failed,
    BotDetected,
    Success
}

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

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

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

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

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

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

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

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

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

Аналітичні дані ніколи не повинні блокувати вашу програму. Уамі. NET включає фоновий відправник за допомогою 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");
            }
        }
    }
}

Події буде додано до черги у пам' яті і оброблено асинхронно. Ваші веб- запити негайно повертаються, аналітичні трапляються у тлі.

Повторіть правила з ввічливістю

Сталася помилка у мережі. Бібліотека використовує Polly для гнучких викликів HTTP:

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

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

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

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 пакунок, тести можуть перевіряти поведінку журналу:

[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:

[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

builder.Services.SetupUmamiClient(builder.Configuration);

Бібліотека читає вашу книгу. appsettings.json:

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

Події стеження

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

Отримати аналітичні дані

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Він відкритий, ретельно перевірений і спроектований, щоб полегшити ваше життя.

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

logo

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