向1.0:使Umami.NET 生产准备就绪 (中文 (Chinese Simplified))

向1.0:使Umami.NET 生产准备就绪

Thursday, 20 November 2025

//

6 minute read

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

当我第一次融入时 Uumami 分析学 在博客平台上, 我很快遇到了一个令人沮丧的现实: Umami 的API 文件是... 让我们做慈善家,把它称为“ 最小 ” 。 错误信息充其量是隐秘的, 最坏情况下是不存在的。 不同版本的参数变化没有警告。 甚至不要让我开始“ 蜂窝” 机器人检测反应。

于是我建造了 Umami. 网 网 - 不仅仅是一个简单的HTTP包装纸,而是一个为制作准备的客户图书馆,可以补偿Umami的所有怪事。

  1. 附有有用的错误信息的全面验证
  2. 强有力的测试基础设施
  3. 真实世界情景情景的明显错误处理

让我来告诉你 是什么让图书馆做好了制作准备

N Nuget 元数 驾照:MIT 网 网 网

问题:Umami的文件差距

这就是你直接和Umami的API合作时 所反对的:

  • 没有输入验证 发送错误的图形用户界面?
  • 隐秘反应 - 检测到Bot了吗? "beep boop"就是这样。
  • 断断更改 - 不同版本之间重新命名的参数(path Vs 和 url, hostname Vs 和 host)
  • 时标混乱 一九九毫秒,二秒,祝你好运
  • JWT答复 有时是满载有效载荷 有时只是访客身份证 没有文件解释时间和原因

这对快速原型来说是好的,但对于生产来说是好的。 你需要更好的东西。

保护条款:与上下文不快

最坏的错误是那些默默无闻的失败。 Ummami.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: 前缀? 每个错误消息都告诉您 哪里出了错如何修补它。这是Umami本应提供的文件。

日期范围校验

当建立分析查询时, 日期范围可能比较复杂。 图书馆会捕捉这些错误 :

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 的 Quirks

"Biep Boop"问题

Umami的机器人检测返回了一个简单的文本响应: "beep boop"JSON不是 JSON 不是正常身份代码 只是...

以下是Umami的处理方式。

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
}

不要再有怪异的回答了 检查一下状况

参数名称更改

Umami 在 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 对象,并让库处理转换。

与频道进行背景处理

分析器不应阻止您的应用程序。 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个错误触发了自动回溯和指数反转。 您的分析对临时网络问题具有适应力。

带有自动检索自动验证的验证

获取分析数据( 不仅仅是发送事件) 时, 您需要认证。 库会自动处理标记过期问题 :

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

你永远不必考虑 象征性管理 - 它只是工作。

测试基础设施

生产准备就绪的代码需要全面测试。

用于日志核查的假 Lolog 跳器

使用微软 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 处理器

Async 测试操作很棘手。 这是使用 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
}

此模式确保 :

  • 实际处理背景活动
  • 在合理时间内完成处理
  • 适当报告模拟处理器的销售情况

全面测试覆盖率

测试套件包括:

  • 配置验证( 无效的 GUID 、 缺失的 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文件
  • NuGet 软件包出版
  • 业绩基准
  • 共同分析查询的其他便利方法

结论 结论 结论 结论 结论

建一个可供制作的图书馆 不仅仅是包一个API, 而是创造一种经验, 更好 Umami.NET弥补了Umami的文件空白,包括:

  • 解释出错的原因以及如何纠正的验证
  • 优雅地处理诡异的API行为
  • 全面测试证明它有效
  • 不妨碍应用程序的背景处理
  • 用自动回调处理有弹性的错误处理

如果你在.NET应用中使用 Umami 分析器,我很乐意你试试 Umami. 网 网它是开放源码, 测试过量, 设计可以让你的生活更轻松。

有问题或建议吗?

Finding related posts...
logo

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