Back to "Umami tracking-client toevoegen 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

Umami tracking-client toevoegen Follow-up

Tuesday, 27 August 2024

Inleiding

In een eerdere post Ik schetste hoe een Tracking Client voor Umami in C# zou kunnen werken. Nou, ik heb eindelijk de kans gehad om het uitgebreid te testen en de werking ervan te verbeteren (ja ANOTHER IHostedService).

Quirks van de Umami API

De Umami Tracking API is zowel zeer eigenzinnig als zeer terse. Dus moest ik de clientcode updaten om het volgende af te handelen:

  1. De API verwacht een'real' looking User-Agent string. Dus moest ik de client updaten om een echte User-Agent string te gebruiken (of om preciezer te zijn heb ik een echte User-Agent string van een browser gevangen en gebruikt).
  2. De API verwacht JSON-invoer in een zeer specifiek formaat; lege tekenreeksen zijn niet toegestaan. Dus moest ik de klant updaten om dit af te handelen.
  3. De Knooppunt API-client heeft een beetje een vreemd oppervlak. Het is niet meteen duidelijk wat de API verwacht. Dus ik moest een beetje trial en error doen om het te laten werken.

De Knooppunt API-client

De Node API client in totaal is hieronder, het is super flexibel maar ECHT niet goed gedocumenteerd.

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;

Zoals u ziet onthult het de volgende methoden:

  1. init - Om de opties in te stellen.
  2. send - Om de lading te sturen.
  3. track - Om een evenement op te sporen.
  4. identify - Om een gebruiker te identificeren.
  5. reset - Om de eigendommen te resetten.

De kern hiervan is de send methode die de lading naar de API stuurt.

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

De C#-client

Om te beginnen heb ik bijna gekopieerd de Node API client's UmamiOptions en UmamiPayload lessen (ik zal ze niet meer passeren ze zijn groot).

Dus nu mijn Send methode ziet er als volgt uit:

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

Er zijn hier twee kritieke delen:

  1. De PopulateFromPayload methode die de lading bevolkt met de websiteId en de eventData.
  2. De JSON-serialisatie van de lading, het moet nul waarden uitsluiten.

De PopulateFromPayload Methode

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

U kunt zien dat wij er altijd voor zorgen dat de websiteId is ingesteld en we stellen de andere waarden alleen in als ze niet nul zijn. Dit geeft ons flexibiliteit ten koste van een beetje verbosheid.

De HttpClient-instellingen

Zoals eerder vermeld moeten we een enigszins echte User-Agent string aan de API geven. Dit wordt gedaan in de HttpClient Installeren.

              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

Achtergronddienst

Dit is nog een andere IHostedService, er zijn een heleboel artikelen over hoe deze op te zetten, zodat ik zal niet gaan in het hier (probeer de zoekbalk!).

Het enige pijnpunt was het gebruik van de geïnjecteerde HttpClient in de UmamiClient Klas. Vanwege het scopen van de client & de dienst die ik gebruikte een IServiceScopeFactory geïnjecteerd in de constructeur van de HostedService dan pak het voor elke verzendaanvraag.

    

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

Gebruik van de Hosted Service

Nu we deze gehoste service hebben, kunnen we de prestaties drastisch verbeteren door de gebeurtenissen op de achtergrond te sturen.

Ik heb dit gebruikt op een paar verschillende plaatsen, in mijn Program.cs Ik besloot om te experimenteren met het volgen van de RSS feed verzoek met behulp van Middleware, het detecteert gewoon elk pad eindigend in 'RSS' en stuurt een achtergrond gebeurtenis.

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

Ik heb ook meer gegevens van mijn TranslateAPI eindpunt. Dat stelt me in staat om te zien hoe lang vertalingen nemen; Merk op dat geen van deze blokkeren de belangrijkste draad of het bijhouden van individuele gebruikers.

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

Conclusie

De Umami API is een beetje eigenzinnig maar het is een geweldige manier om gebeurtenissen op een zelf-gehoste manier te volgen. Hopelijk krijg ik de kans om het nog meer op te ruimen en een Umami nuget pakket te krijgen. Bovendien van een Vorig artikel Ik wil data terughalen uit Umami om functies te bieden zoals populariteit sorteren.

logo

©2024 Scott Galloway