# Adding Umami Tracking Client Follow Up

<!--category-- ASP.NET, Umami -->
<datetime class="hidden">2024-08-27T02:00</datetime>

# Introduzione

In un [post precedente](/blog/addingumamitrackingclient) Ho disegnato come funzionasse un cliente di tracciamento per Umami in C#
Beh, io'ho finalmente avuto l'opportunità di testarlo in modo approfondito e di migliorarlo'il suo funzionamento M SK2sì Un altro `IHostedService`).

[TOC]

# I capricci dell'API Umami

L'API Umami Tracking è sia molto opinionata che molto ridotta. Quindi ho dovuto aggiornare il codice cliente per gestire le cose suivantes:

1. L'API si aspetta di avere un 'realM SK1 che appare User-Agent stringMSC3 Perciò ho dovuto aggiornare il cliente per usare un vero User -Agenting string | | ( | o, per essere più precisi, ho catturato un vero Usuario |- | Agent string da un browser e l'ho usato.
2. L'API lo aspetta' l'input JSON in un formato molto particolare ; le stringhe vuote non sono autorizzate. Perciò ho dovuto aggiornare il cliente per gestire questo
3. L'informazione figura nella parte dispositiva. [Node API client](https://github.com/umami-software/node) Ha un po' di una superficie strana. Non è chiaro subito cosa si aspetta dall'API. Quindi ho dovuto fare qualche prova e errore per farlo funzionare.

## Il cliente API di Node

Il cliente API di Node in totale è inferiore, è super flessibile, ma non documentato bene.

```javascript
export interface UmamiOptions {
  hostUrl?: string;
  websiteId?: string;
  sessionId?: string;
  userAgent?: string;
}

export interface UmamiPayload {
  website: string;
  session?: string;
  hostname?: string;
  language?: string;
  referrer?: string;
  screen?: string;
  title?: string;
  url?: string;
  name?: string;
  data?: {
    [key: string]: string | number | Date;
  };
}

export interface UmamiEventData {
  [key: string]: string | number | Date;
}

export class Umami {
  options: UmamiOptions;
  properties: object;

  constructor(options: UmamiOptions = {}) {
    this.options = options;
    this.properties = {};
  }

  init(options: UmamiOptions) {
    this.options = { ...this.options, ...options };
  }

  send(payload: UmamiPayload, type: 'event' | 'identify' = 'event') {
    const { hostUrl, userAgent } = this.options;

    return fetch(`${hostUrl}/api/send`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': userAgent || `Mozilla/5.0 Umami/${process.version}`,
      },
      body: JSON.stringify({ type, payload }),
    });
  }

  track(event: object | string, eventData?: UmamiEventData) {
    const type = typeof event;
    const { websiteId } = this.options;

    switch (type) {
      case 'string':
        return this.send({
          website: websiteId,
          name: event as string,
          data: eventData,
        });
      case 'object':
        return this.send({ website: websiteId, ...(event as UmamiPayload) });
    }

    return Promise.reject('Invalid payload.');
  }

  identify(properties: object = {}) {
    this.properties = { ...this.properties, ...properties };
    const { websiteId, sessionId } = this.options;

    return this.send(
      { website: websiteId, session: sessionId, data: { ...this.properties } },
      'identify',
    );
  }

  reset() {
    this.properties = {};
  }
}

const umami = new Umami();

export default umami;
```

Come vedete, espone i metodi suivants:

1. `init` - Per stabilire le opzioni.
2. `send` - Per mandare la carico.
3. `track` - Per tracciare un eventoM SK1
4. `identify` - Per identificare un utente.
5. `reset` - Per resettare le proprietà.

Il nocciolo di tutto questo è il `send` Il metodo che manda la carico all'API.

```javascript
  send(payload: UmamiPayload, type: 'event' | 'identify' = 'event') {
    const { hostUrl, userAgent } = this.options;

    return fetch(`${hostUrl}/api/send`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': userAgent || `Mozilla/5.0 Umami/${process.version}`,
      },
      body: JSON.stringify({ type, payload }),
    });
  }
```

# Il cliente C#

Per cominciare ho praticamente copiato il cliente Node API's `UmamiOptions` e `UmamiPayload` classi (Io ho vinto'non li ho superati di nuovo loro ' sono grandiM SK3

Quindi ora il mio `Send` Il metodo sembra questo:

```csharp
     public async Task<HttpResponseMessage> Send(UmamiPayload? payload=null, UmamiEventData? eventData =null,  string type = "event")
        {
            var websiteId = settings.WebsiteId;
             payload = PopulateFromPayload(websiteId, payload, eventData);
            
            var jsonPayload = new { type, payload };
            logger.LogInformation("Sending data to Umami: {Payload}", JsonSerializer.Serialize(jsonPayload, options));

            var response = await client.PostAsJsonAsync("api/send", jsonPayload, options);

            if (!response.IsSuccessStatusCode)
            {
                logger.LogError("Failed to send data to Umami: {StatusCode}, {ReasonPhrase}", response.StatusCode, response.ReasonPhrase);
            }
            else
            {
                var content = await response.Content.ReadAsStringAsync();
                logger.LogInformation("Successfully sent data to Umami: {StatusCode}, {ReasonPhrase}, {Content}", response.StatusCode, response.ReasonPhrase, content);
            }

            return response;
        }

```

Ci sono due parti critiche qui.

1. L'informazione figura nella parte dispositiva. `PopulateFromPayload` metodo che riempie la carico con il sitoID e l'eventoData.
2. La serializzazione JSON della carico di carico, deve escludere i valori nulli.

## L'informazione figura nella parte dispositiva. `PopulateFromPayload` Metoda

```csharp
        public static UmamiPayload PopulateFromPayload(string webSite, UmamiPayload? payload, UmamiEventData? data)
        {
            var newPayload = GetPayload(webSite, data: data);
            if(payload==null) return newPayload;
            if(payload.Hostname != null)
                newPayload.Hostname = payload.Hostname;
            if(payload.Language != null)
                newPayload.Language = payload.Language;
            if(payload.Referrer != null)
                newPayload.Referrer = payload.Referrer;
            if(payload.Screen != null)
                newPayload.Screen = payload.Screen;
            if(payload.Title != null)
                newPayload.Title = payload.Title;
            if(payload.Url != null)
                newPayload.Url = payload.Url;
            if(payload.Name != null)
                newPayload.Name = payload.Name;
            if(payload.Data != null)
                newPayload.Data = payload.Data;
            return newPayload;          
        }
        
        private static UmamiPayload GetPayload(string websiteId, string? url = null, UmamiEventData? data = null)
        {
            var payload = new UmamiPayload
            {
            Website = websiteId,
                Data = data,
                Url = url ?? string.Empty
            };
            

            return payload;
        }

```

Potete vedere che noi ci assicuriamo sempre di `websiteId` è stabilito e noi mettiamo solo gli altri valori se non sono nulli. Questo ci dà flessibilità a spese di un po' di verbosità.

## L'installazione di HttpClient

Come abbiamo detto prima, dobbiamo dare una specie di reale sequenza User-Agent alla API. Questo è fatto nel `HttpClient` configurazione.

```csharp
              services.AddHttpClient<UmamiClient>((serviceProvider, client) =>
            {
                 umamiSettings = serviceProvider.GetRequiredService<UmamiClientSettings>();
            client.DefaultRequestHeaders.Add("User-Agent", $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36");
            client.BaseAddress = new Uri(umamiSettings.UmamiPath);
        }).SetHandlerLifetime(TimeSpan.FromMinutes(5))  //Set lifetime to five minutes
        .AddPolicyHandler(GetRetryPolicy())
       #if DEBUG 
        .AddLogger<HttpLogger>();
        #else
        ;
        #endif

```

## Background Service

Questo è un altro esempio. `IHostedService`, eccomi qui, ' c'è un sacco di articoli su come metterli in piedi così ho vinto.

L'unico punto di dolore era l'injezione. `HttpClient` In questo caso, si tratta di un errore. `UmamiClient` class. A causa della scala del cliente & il servizio ho usato un `IServiceScopeFactory` viene inserito nel costruttore del HostedService e poi lo prende per ogni richiesta di invio.

```csharp
    

    private async Task SendRequest(CancellationToken token)
    {
        logger.LogInformation("Umami background delivery started");

        while (await _channel.Reader.WaitToReadAsync(token))
        {
            while (_channel.Reader.TryRead(out var payload))
            {
                try
                {
                   using  var scope = scopeFactory.CreateScope();
                    var client = scope.ServiceProvider.GetRequiredService<UmamiClient>();
                    // Send the event via the client
                    await client.Send(payload.Payload);

                    logger.LogInformation("Umami background event sent: {EventType}", payload.EventType);
                }
                catch (OperationCanceledException)
                {
                    logger.LogWarning("Umami background delivery canceled.");
                    return; // Exit the loop on cancellation
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "Error sending Umami background event.");
                }
            }
        }
    }
   
```

### Utilizzare il servizio ospitato

Ora che abbiamo questo servizio ospitato, possiamo migliorare drasticamente la performance mandando gli eventi in fondo.

I' l'ho usato in un paio di posti diversi. `Program.cs` Ho deciso di sperimentare con il tracciamento della richiesta di RSS usando Middleware, rileva ogni percorso che finisce in 'RSSM SK2 e manda un evento di fondo .

```csharp
app.Use( async (context, next) =>
{
var path = context.Request.Path.Value;
if (path.EndsWith("RSS", StringComparison.OrdinalIgnoreCase))
{
var rss = context.RequestServices.GetRequiredService<UmamiBackgroundSender>();
// Send the event in the background
await rss.SendBackground(new UmamiPayload(){Url  = path, Name = "RSS Feed"});
}
await next();
});
```

I' ho anche trasmesso altri dati dalla mia `TranslateAPI` punto finale.
Il che mi permette di vedere quanto tempo ci vogliono le traduzione; nota nessuno di questi blocca il thread principale O traccia gli utenti individuali.

```csharp
    
       await  umamiClient.SendBackground(new UmamiPayload(){  Name = "Get Translation"}, new UmamiEventData(){{"timetaken", translationTask.TotalMilliseconds}, {"language",translationTask.Language}});
        var result = new TranslateResultTask(translationTask, true);
```

# Conclusione

L'API di Umami è un po' bizzarro ma è'un ottimo modo per tracciare gli eventi in modo autonomo-in modo gestitoM SK2 Speriamo che ioMSC3 avrò la possibilità di pulirlo ancora più e di portare fuori un pacco di nuget di UmimiMスク4
Inoltre, da un [precedente articolo](/blog/addingascsharpclientforumamiapi) Voglio tirare fuori i dati da Umami per fornire funzioni come la classificazione della popolarità.