NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Tuesday, 27 August 2024
//Less than a minute
In un posto precedente Ho abbozzato come un Cliente di Tracciamento per Umami in C# potrebbe funzionare.
Beh, ho finalmente avuto la possibilità di testarlo ampiamente e migliorare il suo funzionamento (sì UN'ALTRA) IHostedService
).
L'API Umami Tracking è molto apprezzata e molto terse. Quindi ho dovuto aggiornare il codice client per gestire quanto segue:
Il client Nodo API in totale è sotto, è super flessibile, ma Davvero non ben documentato.
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 esporre i seguenti metodi:
init
- Per impostare le opzioni.send
- Per mandare il carico.track
- Per rintracciare un evento.identify
- Per identificare un utente.reset
- Per resettare le proprieta'.Il nucleo di questo è il send
metodo che invia il carico utile 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 }),
});
}
Per cominciare ho copiato praticamente il client delle API Node UmamiOptions
e UmamiPayload
classi (non li supererò di nuovo sono grandi).
Così ora il mio Send
metodo assomiglia a 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;
}
Qui ci sono due parti critiche:
PopulateFromPayload
metodo che popola il carico utile con il sito webId e l'eventoData.PopulateFromPayload
Metodo 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 ci assicuriamo sempre che websiteId
è impostato e fissiamo gli altri valori solo se non sono nulli. Questo ci dà flessibilità a scapito di un po 'di verbosità.
Come accennato prima dobbiamo dare una stringa User-Agent un po' reale all'API. Questo è fatto nel HttpClient
Prepararsi.
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
Questo è un altro IHostedService
, ci sono un sacco di articoli su come impostare questi in modo da non entrare in questo qui (provare la barra di ricerca!).
L'unico punto di dolore era l'uso dell'iniezione HttpClient
Nella UmamiClient
classe. A causa dello scoping del client e del servizio che ho usato IServiceScopeFactory
iniettato nel costruttore del HostedService poi afferrare 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.");
}
}
}
}
Ora che abbiamo questo servizio ospitato, possiamo migliorare notevolmente le prestazioni inviando gli eventi in background.
Ho usato questo in un paio di posti diversi, nel mio Program.cs
Ho deciso di sperimentare con il monitoraggio della richiesta di feed RSS utilizzando Middleware, rileva solo qualsiasi percorso che termina in 'RSS' e invia un evento di background.
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();
});
Ho anche passato più dati dalla mia TranslateAPI
Endpoint.
Che mi permette di vedere quanto tempo le traduzioni stanno prendendo; notare nessuno di questi stanno bloccando il thread principale O tracciando i singoli utenti.
await umamiClient.SendBackground(new UmamiPayload(){ Name = "Get Translation"}, new UmamiEventData(){{"timetaken", translationTask.TotalMilliseconds}, {"language",translationTask.Language}});
var result = new TranslateResultTask(translationTask, true);
L'API Umami è un po' strana, ma è un ottimo modo per monitorare gli eventi in modo auto-ospitato. Speriamo di avere la possibilita' di ripulire ancora di piu' e prendere un pacco di nuget Umami la' fuori. Inoltre da un articolo precedente Voglio estrarre i dati da Umami per fornire caratteristiche come lo smistamento della popolarità.