# Umami.Net och Bot-detektor

# Inleiding

Så [postade en LITT](/blog/category/Umami) i det förflutna om att använda Umami för analytik i en självstyrd miljö och till och med publicerade [Umami.Net Nuget pacakge](https://www.nuget.org/packages/Umami.Net/). Men jag hade ett problem där jag ville följa användarna av min RSS-ström.

[TOC]

<!--category-- ASP.NET, Umami -->
<datetime class="hidden">2024-09-12T14:50</datetime>

# Problemet

Problemet är att RSS-feed-reader försöker gå igenom *användbar* Användaragenter när man frågar om flödet. Det låter **följande** för att spåra antalet användare och typen av användare som konsumerar flödet *robot* begäran. Det här är ett problem för mig eftersom det leder till att begäran ignoreras och inte spåras

Feedbins användaragent ser ut så här:

```plaintext
Feedbin feed-id:1234 - 21 subscribers
```

Så ganska användbar rätt, den skickar några användbara detaljer om vad din feed-id är , antalet användare och användaragenten . Men också detta är ett problem, eftersom det betyder att Umami kommer att ignorera begäran `{"beep": "boop"}` vilket betyder att det är identifierat som en bot-ansökning.

När vi tittar på Umami-dashboarden efter att ha gjort den här förändringen så kan jag nu se de ursprungliga UserAgents som bad om flödet

![Umami-dashboard](user-agents.png?width=600&format=webp&quality=30)

Så vi kan se där en grupp som skulle ha aktiverat robotdetektorn men nu kan vi se dessa och följa upp begäran.

# Lösningen

Så vad är lösningen på detta? ? Jag kan, ' jag kan inte manuell analysera alla dessa begäran och upptäcka om Umami kommer att upptäcka dem som en bot. ; det använder IsBot. ( https :// www . npmjs . com / paket / isbot ) för att upptäcke om en begäran är en bot eller ej . Det finns inget C- # likvärdigt och det är en ändrad lista så jag kan ' inte ens använda den listan.
Så jag måste interceptera begäran innan den kommer till Umami och ändra användaragenten till något som Umami kommer att acceptera för specifika begäran.

Så nu har jag lagt till några extra parametrar till mina spårningsmetoder i Umami.NetM SK1 De låter dig specifikera den nya 'Default User AgentMska3 som kommer skickas till Umami istället för den ursprungliga User AgentenM Ska4 Det låter mig specifikera att användaragenten ska ändras till en specifik värde för specifika begäran.

## Metoderna

På min `UmamiBackgroundSender` Jag lade till följande:

```csharp
   public async Task TrackPageView(string url, string title, UmamiPayload? payload = null,
        UmamiEventData? eventData = null, bool useDefaultUserAgent = false)
```

Detta existerar på alla spårningsmetoder där och bara sätter en parameter på `UmamiPayload` objekt.

På `UmamiClient` dessa kan sättas så här::

```csharp
    [Fact]
    public async Task TrackPageView_WithUrl()
    {
        var handler = JwtResponseHandler.Create();
        var umamiClient = GetServices(handler);
        var response = await umamiClient.TrackPageViewAndDecode("https://example.com", "Example Page",
            new UmamiPayload { UseDefaultUserAgent = true });
        Assert.NotNull(response);
        Assert.Equal(UmamiDataResponse.ResponseStatus.Success, response.Status);
    }
```

I denna test använder jag den nya `TrackPageViewAndDecode` metod som återvänder en `UmamiDataResponse` objekt. Det här objektet innehåller avkodad JWT-token | ( | som är ovärderligt om det är en robot, så det är användbart att kontrollera |

## `PayloadService`

Det här är allt hanterat i `Payload` tjänst som är ansvarig för att fylla i payload-objektet. Det här är där `UseDefaultUserAgent` är ställt.

Standardmässigt lägger jag upp payloaden från `HttpContext` så du usually får den här uppsättningen korrekt.

```csharp
    private UmamiPayload GetPayload(string? url = null, UmamiEventData? data = null)
    {
        var httpContext = httpContextAccessor.HttpContext;
        var request = httpContext?.Request;

        var payload = new UmamiPayload
        {
            Website = settings.WebsiteId,
            Data = data,
            Url = url ?? httpContext?.Request?.Path.Value,
            IpAddress = httpContext?.Connection?.RemoteIpAddress?.ToString(),
            UserAgent = request?.Headers["User-Agent"].FirstOrDefault(),
            Referrer = request?.Headers["Referer"].FirstOrDefault(),
            Hostname = request?.Host.Host
        };

        return payload;
    }
```

Då har jag en bit kod som heter `PopulateFromPayload` som är där applikationsobjektet får det

```csharp
    public static string DefaultUserAgent =>
        $"Mozilla/5.0 (Windows 11)  Umami.Net/{Assembly.GetAssembly(typeof(UmamiClient))!.GetName().Version}";

    public UmamiPayload PopulateFromPayload(UmamiPayload? payload, UmamiEventData? data)
    {
        var newPayload = GetPayload(data: data);
        ...
        
        newPayload.UserAgent = payload.UserAgent ?? DefaultUserAgent;

        if (payload.UseDefaultUserAgent)
        {
            var userData = newPayload.Data ?? new UmamiEventData();
            userData.TryAdd("OriginalUserAgent", newPayload.UserAgent ?? "");
            newPayload.UserAgent = DefaultUserAgent;
            newPayload.Data = userData;
        }


        logger.LogInformation("Using UserAgent: {UserAgent}", newPayload.UserAgent);
     }        
        
```

Du kommer att se att detta definierar en ny Useragent längst upp i filet ( som jag har bekräftat är *i dagsläget* upptäckt som en bot). Sen i metoden upptäcker den om antingen UserAgent är ofull ( vilket inte skulle kunna hända, om det inte är ' som kallas från kod utan HttpContext `UseDefaultUserAgent` är ställt. Om det är så ställer den UserAgent till standard och lägger till den ursprungliga UserAgenten i dataobjektet

Detta registreras så att du kan se vad UserAgent används.

## Dekodering av svaret.

I Umami added jag ett antal nya 'AndDecode' metoder som återvänder `UmamiDataResponse` objekt. Det här objektet innehåller den dekoderade JWT-tokenen

```csharp
    public async Task<UmamiDataResponse?> TrackPageViewAndDecode(
        string? url = "",
        string? title = "",
        UmamiPayload? payload = null,
        UmamiEventData? eventData = null)
    {
        var response = await TrackPageView(url, title, payload, eventData);
        return await DecodeResponse(response);
    }
    
        private async Task<UmamiDataResponse?> DecodeResponse(HttpResponseMessage responseMessage)
    {
        var responseString = await responseMessage.Content.ReadAsStringAsync();

        switch (responseMessage.IsSuccessStatusCode)
        {
            case false:
                return new UmamiDataResponse(UmamiDataResponse.ResponseStatus.Failed);
            case true when responseString.Contains("beep") && responseString.Contains("boop"):
                logger.LogWarning("Bot detected data not stored in Umami");
                return new UmamiDataResponse(UmamiDataResponse.ResponseStatus.BotDetected);

            case true:
                var decoded = await jwtDecoder.DecodeResponse(responseString);
                if (decoded == null)
                {
                    logger.LogError("Failed to decode response from Umami");
                    return null;
                }

                var payload = UmamiDataResponse.Decode(decoded);

                return payload;
        }
    }
```

Ni kan se att det här låter in i det normala `TrackPageView` metoden kallar sedan en metod som kallas `DecodeResponse` som kontrollerar svaret på `beep` och `boop` strängar ( för botdetektorn ). Om den hittar dem så registrerar den en varning `BotDetected` status. Om den inte hittar dem, så avkoder den JWT-token och ger tillbaka payloadet

JWT-token i sig är bara en koderad String med basen, 64, som innehåller data som Umami har sparat. `UmamiDataResponse` objekt.

Den kompletta källan för detta är under

<details>
<summary>Response Decoder</summary>

```csharp
using System.IdentityModel.Tokens.Jwt;

namespace Umami.Net.Models;

public class UmamiDataResponse
{
    public enum ResponseStatus
    {
        Failed,
        BotDetected,
        Success
    }

    public UmamiDataResponse(ResponseStatus status)
    {
        Status = status;
    }

    public ResponseStatus Status { get; set; }

    public Guid Id { get; set; }
    public Guid WebsiteId { get; set; }
    public string? Hostname { get; set; }
    public string? Browser { get; set; }
    public string? Os { get; set; }
    public string? Device { get; set; }
    public string? Screen { get; set; }
    public string? Language { get; set; }
    public string? Country { get; set; }
    public string? Subdivision1 { get; set; }
    public string? Subdivision2 { get; set; }
    public string? City { get; set; }
    public DateTime CreatedAt { get; set; }
    public Guid VisitId { get; set; }
    public long Iat { get; set; }

    public static UmamiDataResponse Decode(JwtPayload? payload)
    {
        if (payload == null) return new UmamiDataResponse(ResponseStatus.Failed);
        payload.TryGetValue("visitId", out var visitIdObj);
        payload.TryGetValue("iat", out var iatObj);
        //This should only happen then the payload is dummy.
        if (payload.Count == 2)
        {
            var visitId = visitIdObj != null ? Guid.Parse(visitIdObj.ToString()!) : Guid.Empty;
            var iat = iatObj != null ? long.Parse(iatObj.ToString()!) : 0;

            return new UmamiDataResponse(ResponseStatus.Success)
            {
                VisitId = visitId,
                Iat = iat
            };
        }

        payload.TryGetValue("id", out var idObj);
        payload.TryGetValue("websiteId", out var websiteIdObj);
        payload.TryGetValue("hostname", out var hostnameObj);
        payload.TryGetValue("browser", out var browserObj);
        payload.TryGetValue("os", out var osObj);
        payload.TryGetValue("device", out var deviceObj);
        payload.TryGetValue("screen", out var screenObj);
        payload.TryGetValue("language", out var languageObj);
        payload.TryGetValue("country", out var countryObj);
        payload.TryGetValue("subdivision1", out var subdivision1Obj);
        payload.TryGetValue("subdivision2", out var subdivision2Obj);
        payload.TryGetValue("city", out var cityObj);
        payload.TryGetValue("createdAt", out var createdAtObj);

        return new UmamiDataResponse(ResponseStatus.Success)
        {
            Id = idObj != null ? Guid.Parse(idObj.ToString()!) : Guid.Empty,
            WebsiteId = websiteIdObj != null ? Guid.Parse(websiteIdObj.ToString()!) : Guid.Empty,
            Hostname = hostnameObj?.ToString(),
            Browser = browserObj?.ToString(),
            Os = osObj?.ToString(),
            Device = deviceObj?.ToString(),
            Screen = screenObj?.ToString(),
            Language = languageObj?.ToString(),
            Country = countryObj?.ToString(),
            Subdivision1 = subdivision1Obj?.ToString(),
            Subdivision2 = subdivision2Obj?.ToString(),
            City = cityObj?.ToString(),
            CreatedAt = createdAtObj != null ? DateTime.Parse(createdAtObj.ToString()!) : DateTime.MinValue,
            VisitId = visitIdObj != null ? Guid.Parse(visitIdObj.ToString()!) : Guid.Empty,
            Iat = iatObj != null ? long.Parse(iatObj.ToString()!) : 0
        };
    }
}
```

</details>
Ni kan se att det här innehåller en massa användbara information om den begäran som Umami har sparat.. Om du till exempel ville visa olika innehåll baserat på lokalisering, så låter detta dig göra det.

```csharp
    public Guid Id { get; set; }
    public Guid WebsiteId { get; set; }
    public string? Hostname { get; set; }
    public string? Browser { get; set; }
    public string? Os { get; set; }
    public string? Device { get; set; }
    public string? Screen { get; set; }
    public string? Language { get; set; }
    public string? Country { get; set; }
    public string? Subdivision1 { get; set; }
    public string? Subdivision2 { get; set; }
    public string? City { get; set; }
    public DateTime CreatedAt { get; set; }
    public Guid VisitId { get; set; }
    public long Iat { get; set; }
```

# I slutsats

Så bara en kort artikel som innefattar några nya funktioner i Umami.Net | 0.4.0 | som tillåter dig att speciera en standardanvändare agent för specifika begäran |. | Detta är användbart för att spåra begärningar som Umami annars skulle ignorera