Back to "نحو 1: جعل نظام إمامي. شبكة الإنتاج جاهز"

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: جعل نظام إمامي. شبكة الإنتاج جاهز

Thursday, 20 November 2025

أولاً

عندما أدمج أولاً أُمُمْيِيَامِي مُحَلِّيّات إلى منصة مدونتي، وسرعان ما صادفت حقيقة محبطة: وثائق AMAMI API هي... دعونا نكون خيريين ونسميها "الحد الأدنى". الرسائل الخاطئة غامضة في أفضل الأحوال، غير موجودة في أسوأ الأحوال. تغير البارامترات بين النسخ بدون تحذير. ولا تجعلني أبدأ حتى في الاستجابة الكشفية "Beep boop".

لذلك بنيت الأم (الوطن) (الوطن) ليس فقط كملف HTTTP بسيط، ولكن كمكتبة عملاء جاهزة للانتاج التي تعوض عن كل مناورات أميمي

  1. المصادق الشامل مع رسائل
  2. الهياكل الأساسية للاختبار
  3. عُدِلَة الخطأ المُسْرَح لسينيات العالم الحقيقي

اسمحوا لي أن أطلعكم على ما يجعل هذه المكتبة جاهزة للإنتاج.

الرخصة: MIT الصافي الصافي

المشكلة: النقص في الوثائق المتعلقة بأمامي

إليكم ما تواجهونه عندما تعملون مع AOMAME's API مباشرة:

  • لا يوجد مُصدِِِِِِِِِِِِِِِِِِِ فشل صامت
  • الردود المباركة -هل تم كشفها؟ "beep boop"هذا هو عليه.
  • **** - البارامترات التي يُعاد تسميتها بين نسخpath vs url, hostname vs host)
  • الترمل -الثواني، حظاً موفقاً في معرفة ذلك
  • ردود JWT JWT -أحياناً حمولة كاملة، وأحياناً بطاقة هوية للزائرين فقط، لا وثائق توضح متى أو لماذا.

هذا مناسب لنموذج أولي سريع، لكن للإنتاج؟ تحتاج إلى شيء أفضل.

&: فشل بالفور مع السياق

أسوأ الحشرات هي تلك التي تفشل بصمت. 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إذا كان تشكيلك خاطئاً، فأنت تعرف فوراً - ليس عندما يحاول أول حدث تحليلي إرساله.

مقدّم الطلب مع تقديم اقتراحات مضونة

لكن السّحر الحقيقي موجود في مساعد نص الإستفسار. تأكّد من هذه الرسائل:

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: كل خطأ غير موجود يُخبرك ما حدث خطأ وقد عقد مؤتمراً بشأن كيف يمكن إصلاحههذه هي الوثائق التي كان يجب أن تقدمها أمامي

&

عند بناء الإستفسارات التحليلية، نطاقات التاريخ يمكن أن تكون صعبة. المكتبة تلتقط هذه الأخطاء من أجلك:

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"ليس (جيسون)، ليس رمزاً ملائماً للأوضاع فقط...

إليكم كيف تتعامل الشبكة مع الأمر:

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
}

لا مزيد من الإستجابات الغريبة فقط تحقق من الحالة

& لم لم لم لم لم لم

تم إعادة تسمية البارامترات بين إصدارات API. هل قاموا بتوثيق هذا؟ بالطبع لا. المكتبة تتعامل مع كل من:

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

تحويلات

أمامي يستخدم يونيكس ميلي الثانية للأوقات. ها هو مساعد يجعله غير مؤلم:

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

الأحداث تظهر في الذاكرة وتعالج بشكل متزامن. طلبات الويب الخاصة بك تعود على الفور، التحليلات تحدث في الخلفية.

إعادة طلب سياسات مع بولي

فشل الشبكة يحدث. المكتبة تستخدم بولي لـ 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);
}

لا يجب عليك أبداً أن تفكر في الإدارة الرمزية - إنها تعمل فقط.

الاختبار في الهياكل الأساسية

الشفرة المنتجة الجاهزة تحتاج إلى اختبارات شاملة. إليكم ما بنيته:

مُشْفِق مُلَفِش مُلَفِش مُلَفِش مُلَفِش مُلَفِث

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

مور هذا هذا هذا هذا

اختبار عمليات asyync هو صعب. هذا هو نمط يستخدم 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
}

يكفل هذا النمط ما يلي:

  • وقد تم تجهيز وقائع وقائع الوقائع وقائع الاجتماعات
  • إتمام التجهيز في غضون وقت معقول
  • الإبلاغ على نحو سليم عن حالات الطمأنينة في المعالج

التغطية الشاملة للاختبار الشامل

يُغطي خانة الاختبار ما يلي:

  • مصادقة ال_مصادقة على الضبط (المصادقة على غير صحيحDsGids, URs مفقود)
  • □ متابعة حدث مع البيانات وبدونها
  • تَعَقُّب عرض الصفحة على الصفحة
  • تعريف المستخدم
  • □ مناولة الكشف عن بوت
  • □ رد الفريق المشترك بشأن الترميز
  • □ تجهيز المعلومات الأساسية بالفترات الزمنية
  • مصادقة على مدى التاريخ
  • & س س س س س س س س س س س س س س س س
  • □ الموثِّق والعربة المرّة
  • البيانات المسترجعة من القياسات والصفحات

مَنْ مَنْ مَنْ مَنْ مَنْ مَنْ العالم

هذا هو مدى بساطة استخدامها في تطبيق 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؟

المكتبة عبارة عن ميزة كاملة وتجربة معركة في إنتاج هذه المدونة ذاتها. قبل إصدار 1.0، أنا أركز على:

  • الوثائق المتعلقة برقم
  • نشر الحزمة NuuGet
  • مقاييس الأداء
  • □ طرق تيسير إضافية للاستفسارات المتعلقة بالتحليلات المشتركة

ثالثاً - استنتاج

بناء مكتبة جاهزة للانتاج ليس فقط عن تغليف API -- بل عن خلق تجربة تحسين تحسين وتعوض شبكة أمومي عن الثغرات في وثائق أمامي بما يلي:

  • التصديق الذي يفسر ما حدث من خطأ وكيفية إصلاحه
  • المناولة المُسَرَّة للسلوكيات المُنَافِسَة المُتَعَلِّقَة
  • الاختبار الشامل الذي يثبت نجاحه
  • معالجة المعلومات الأساسية التي لا تعيق تطبيقك
  • منفذ الخطأ مناولة مع

إذا كنت تستخدم تحليل أومامي في تطبيق على شبكة الإنترنت، أود منك أن تحاول الأم (الوطن) (الوطن)إنه مصدر مفتوح، تم اختباره بشدة، ومصمم لجعل حياتك أسهل.

فَأَفْتَحْ مَسْؤُولِيَّةً عَلَى ٱلْجِيْتِهُوبِ أَوْ تَطْلُبُ مِنَ ٱلْمُسَاعَدَةِ فِي ٱلْمُسَاعَدَاتِ ٱلتَّالِيَةِ !

logo

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