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
使[email protected]生产就绪
Thursday, 20 November 2025
一. 导言导言 导言 导言 导言
当我第一次融入时 Uumami 分析学 我很快进入了一个令人沮丧的现实 :Ummi'#ASAPI文件是 @...}让's成为慈善家,称它为@"ñminimal."错误信息充其量是加密的 ,(非-)
于是我建了 网路网络 -不只是一个简单的 HTTP 包装,,而且是一个制作的“-备妥”客户库。 该图书馆补偿了所有Umami}'s quirksMS K4。
- 附有有用的错误信息的全面验证
- 强有力的测试基础设施
- 用于真实@-%world 假想的优异错误处理
让我来介绍一下 图书馆的制作内容
问题 @: @Umami'}文件空白
在与乌马米合作时,
- 没有输入验证 @ -_ 发送格式错误的 GUID MS K1} 沉默失败@ .
- 隐秘反应 -=YTET -伊甸园字幕组=- 翻译:
"beep boop""M."那麽"MSSK1"它. - 断开更改 @-_% 参数在版本中重新命名@(}
pathVs 和url,hostnameVs 和host) - 时标混乱 -+Unix毫秒=?+IIs@?}祝您好运
- JWT答复 有时是满载有效载荷 有时候只是一位访客 ID=.}没有文件解释时间和原因
这对快速原型@ , @ but for producting @?}你需要更好的东西吗?
保护条款 *:因上下文无法快速
最坏的错误是那些在启动时 默无声失灵的“.”Umami.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. 如果您的配置错误@, 您马上知道 @-不是当第一个分析事件试图发送.}
附有有益建议的请求验证
但真正的魔法在于查询字符串助手@. @ check out the maddress messages\ :
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应该提供的文件 .
日期范围校验
当建立分析查询时 @ , @ 日期范围可以是 trick .} 图书馆会捕捉这些错误 。 @ I: @
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).");
}
}
处理 Umami's Quirks
@ "BIP BOOP"}问题
Umami's bott 检测返回纯文本响应@: "beep boop"不是Json . 没有正确的状态代码
这里的' @%s 如何使用 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);
}
}
您的代码得到一个干净的 enum@ : @
public enum ResponseStatus
{
Failed,
BotDetected,
Success
}
不再分析怪异的回答@- @ check the status #.#
参数名称更改
Umami 在 API 版本之间重新命名的参数@. @ 是否在文档中记录了此 ?} 当然不是 图书馆处理两个:}
// Support both old and new parameter names
request.Path = queryParams["path"] ?? queryParams["url"];
request.Hostname = queryParams["hostname"] ?? queryParams["host"];
时间戳转换
Umami 使用 Unix 毫秒作为时间戳@ . @ here @ MS K1_ 帮助器, 它能使其无痛 @ MPK2 @
public static long ToMilliseconds(this DateTime dateTime)
{
var dateTimeOffset = new DateTimeOffset(dateTime.ToUniversalTime());
return dateTimeOffset.ToUnixTimeMilliseconds();
}
现在你可以正常工作了 DateTime 对象,并让库处理转换@.
与频道进行背景处理
分析器不应阻止您的应用程序@. @Umami.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 重试政策
网络失败发生@. 图书馆使用 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”错误触发自动重试,以指数反转@. 您的分析对临时网络问题具有适应性 @.
使用 Auto@ - retro 验证
获取分析数据时, 请不要仅仅发送事件@ ),} 您需要认证 @ MS K2} 库会自动处理标记过期@ MPK3}
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);
}
你永远不必考虑象征性管理 - 它只是管用
测试基础设施
即时代码需要全面测试
用于日志核查的假 LogLoggger
使用微软@' @%s FakeLogger @:@% 1
[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 处理器
测试 async 操作是 trig @ . 此处}'%s a type 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
}
此模式确保@ : @%
- 实际处理背景活动
- 在合理时间内完成处理
- 适当报告模拟处理器的销售情况
全面测试覆盖率
测试套件覆盖@: @
- ✅ 配置验证@(invalid GUIDs @, 缺失的 URLs)
- “✅”事件跟踪,有数据或无数据
- ✅页面视图跟踪
- ✅ 用户识别
- ✅Bot检测处理
- ✅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?”的下一步是什么?
图书馆的功能是 @-#complete and great=-}在制作此博客前,
- 📝综合API文件
- 🎯Nuget 软件包出版
- 业绩基准
- 用于常见分析查询的额外便利方法
结论结论
@-_ready library is '}库只是关于包扎 API <%- > It'}有关创造一种经验来创建这样的体验 更好 直接使用 API 来弥补 Umami ' 的文件缺口
- 解释出错的原因以及如何纠正的验证
- 优雅地处理诡异的API行为
- 全面测试证明它有效
- 阻碍您的应用程序的背景处理@ ' @ t
- 用自动回调处理有弹性的错误
如果您在“.NET 应用程序”中使用 Umami 分析器 @ , I'd愿意尝试 网路网络@.@ it' @s open source #,#}重度測試的, @signative to make your life lifer_.#
在 GitHub 上打开一个问题,