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
Thursday, 20 November 2025
Коли я вперше інтегрований Аналітичні аналітики у Умамі У моїй платформі блогу я швидко натрапив на розчаруючу реальність: документація з API Амамі... давайте будемо благодійними і назвемо її "мініальними." Повідомлення про помилки у найкращому випадку закриті, невиявлені у найгіршому. Параметри змінюються між версіями без попередження. І навіть не дайте мені розпочати з відповіді на визначення "beep butp" bot.
Отже, я побудував Umami. NET - не просто як проста обгортка HTTP, а як виробна клієнтська бібліотека, яка компенсує всі примхи Умамі. Оскільки я працювала над випуском версії 1. 0, я зосередилася на трьох важливих ділянках:
Дозвольте мені провести вас через те, що робить цю бібліотеку готовою до виробництва.
Ось проти чого вам доводиться працювати з API Умамі прямо:
"beep boop"Ось так.path всunit description in lists url, hostname всunit description in lists host)Це добре для швидкого прототипу, але для виробництва?
Найгірші вади - це ті, які безшумно зазнають невдачі. 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).");
}
}
Виявлення 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);
}
Ви ніколи не повинні думати про управління ключами - це просто працює.
Виготовлений-готовий код потребує комплексних тестів. Ось що я побудував:
Користування 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));
}
Тестування асинхронних операцій є складним. Ось шаблон за допомогою 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
}
Цей шаблон забезпечує:
Комплекс тестів покриває:
Ось як просто користуватися у програмі ASP. NET:
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 я фокусуюся на:
Побудова бібліотеки, готової до виробництва, - це не просто обгортка API, а створення досвіду. краще ніж безпосередньо користуватися API. Amami. NET компенсує пропуски документації Умамі:
Якщо ви використовуєте аналітичні аналітичні засоби .NET, я хотів би, щоб ви спробували Umami. NETВін відкритий, ретельно перевірений і спроектований, щоб полегшити ваше життя.
Питання або пропозиції: розгорніть GitHub або запишіть нижченаведені коментарі!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.