# 使Umami@.NET生产就绪

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

## 一. 导言导言 导言 导言 导言

当我第一次融入时 [Uumami 分析学](https://umami.is/) 我很快进入了一个令人沮丧的现实 :Ummi'#ASAPI文件是 @...}让's成为慈善家,称它为@"ñminimal."错误信息充其量是加密的 ,(非-)

于是我建了 [网路网络](https://github.com/scottgal/mostlylucidweb/tree/main/Umami.Net) -不只是一个简单的 HTTP 包装,,而且是一个制作的“-备妥”客户库。 该图书馆补偿了所有Umami}'s quirksMS K4。

1. **附有有用的错误信息的全面验证**
2. **强有力的测试基础设施**
3. **用于真实@-\%world 假想的优异错误处理**

让我来介绍一下 图书馆的制作内容

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

## 问题 @: @Umami'}文件空白

在与乌马米合作时,

- **没有输入验证** @ -_ 发送格式错误的 GUID MS K1} 沉默失败@ .
- **隐秘反应** -=YTET -伊甸园字幕组=- 翻译: `"beep boop"`"M."那麽"MSSK1"它.
- **断开更改** @-_% 参数在版本中重新命名@(}`path` Vs 和 `url`, `hostname` Vs 和 `host`)
- **时标混乱** -+Unix毫秒=?+IIs@?}祝您好运
- **JWT答复** 有时是满载有效载荷 有时候只是一位访客 ID=.}没有文件解释时间和原因

这对快速原型@ , @ but for producting @?}你需要更好的东西吗?

## 保护条款 *:因上下文无法快速

最坏的错误是那些在启动时 默无声失灵的“.”Umami.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`. 如果您的配置错误@, 您马上知道 @-不是当第一个分析事件试图发送\.}

### 附有有益建议的请求验证

但真正的魔法在于查询字符串助手@. @ check out the maddress messages\ :

```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:` 前缀@ ?}所有错误消息都告诉你 **哪里出了错** 和 **如何修补它**这是Umami应该提供的文件 .

### 日期范围校验

当建立分析查询时 @ , @ 日期范围可以是 trick .} 图书馆会捕捉这些错误 。 @ I: @

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

## 处理 Umami's Quirks

### @ "BIP BOOP"}问题

Umami's bott 检测返回纯文本响应@: `"beep boop"`不是Json . 没有正确的状态代码

这里的' @%s 如何使用 Umami.}NET 处理@:}

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

您的代码得到一个干净的 enum@ : @

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

不再分析怪异的回答@- @ check the status #.#

### 参数名称更改

Umami 在 API 版本之间重新命名的参数@. @ 是否在文档中记录了此 ?} 当然不是{.} 图书馆处理两个:}

```csharp
// 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 @

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

现在你可以正常工作了 `DateTime` 对象,并让库处理转换@.

## 与频道进行背景处理

分析器不应阻止您的应用程序@. @Umami.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 重试政策

网络失败发生@. 图书馆使用 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”错误触发自动重试,以指数反转@. 您的分析对临时网络问题具有适应性 @.

## 使用 Auto@ - retro 验证

获取分析数据时, 请不要仅仅发送事件@ ),} 您需要认证 @ MS K2} 库会自动处理标记过期@ MPK3}

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

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

## 测试基础设施

即时代码需要全面测试

### 用于日志核查的假 LogLoggger

使用微软@' @%s `FakeLogger` @:@% 1

```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 处理器

测试 async 操作是 trig {} @ . 此处}'%s a type `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
}
```

此模式确保@ : @%

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

### 全面测试覆盖率

测试套件覆盖@: @

- ✅ 配置验证@(invalid GUIDs @, 缺失的 URLs)
- “✅”事件跟踪,有数据或无数据
- ✅页面视图跟踪
- ✅ 用户识别
- ✅Bot检测处理
- ✅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?”的下一步是什么?

图书馆的功能是 @-#complete and great=-}在制作此博客前,

- 📝综合API文件
- 🎯Nuget 软件包出版
- 业绩基准
- 用于常见分析查询的额外便利方法

## 结论结论

@-_ready library is '}库只是关于包扎 API <%- > It'}有关创造一种经验来创建这样的体验 **更好** 直接使用 API 来弥补 Umami ' 的文件缺口

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

如果您在“.NET 应用程序”中使用 Umami 分析器 @ , I'd愿意尝试 [网路网络](https://github.com/scottgal/mostlylucidweb/tree/main/Umami.Net)@.@ it' @s open source #,#}重度測試的, @signative to make your life lifer_.#

在 GitHub 上打开一个问题,