Back to "Adding Umami Tracking Client Follow Up"

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

ASP.NET Umami

Adding Umami Tracking Client Follow Up

Tuesday, 27 August 2024

Introduzione

In un post precedente 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).

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 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.

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.

  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:

     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

        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.

              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.

    

    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 .

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.

    
       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 Voglio tirare fuori i dati da Umami per fornire funzioni come la classificazione della popolarità.

logo

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