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
I en tidigare inlägg Jag skissade upp hur en spårningsklient för Umami i C# kunde fungera.
Jag har äntligen fått en chans att testa den grundligt och förbättra dess funktion (ja ANothER IHostedService
).
Umami Tracking API är både mycket åskådning och mycket terse. Så jag var tvungen att uppdatera klientkoden för att hantera följande:
Node API-klienten totalt är nedan, det är super flexibel men verkligen inte väl dokumenterad.
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;
Som ni ser exponerar det följande metoder:
init
- För att ställa in alternativen.send
- Skicka nyttolasten.track
- För att spåra en händelse.identify
- För att identifiera en användare.reset
- För att återställa fastigheterna.Kärnan i detta är send
metod som skickar nyttolasten till API:et.
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 }),
});
}
Till att börja med kopierade jag i stort sett Node API-klientens UmamiOptions
och UmamiPayload
klasser (jag kommer inte förbi dem igen de är stora).
Så nu är jag min Send
metoden ser ut så här:
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;
}
Det finns två kritiska delar här:
PopulateFromPayload
Metod som fyller nyttolasten med websiteId och eventData.PopulateFromPayload
Metod 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;
}
Du kan se att vi alltid ser till att websiteId
är inställd och vi ställer bara in de andra värdena om de inte är ogiltiga. Detta ger oss flexibilitet på bekostnad av lite verbositet.
Som tidigare nämnts måste vi ge en något verklig User-Agent sträng till API. Detta görs i HttpClient
Uppställning.
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
Detta är ännu en IHostedService
, Det finns en massa artiklar om hur man ställer upp dessa så jag inte går in i det här (försök sökfältet!)..............................................................................................
Den enda smärtpunkten var att använda den injicerade dosen. HttpClient
I bilaga I till förordning (EU) nr 1094/2010 ska följande punkt läggas till: UmamiClient
Klassen. På grund av scoping av klienten & tjänsten jag använde en IServiceScopeFactory
injiceras i konstruktören av HostedService sedan ta den för varje skicka begäran.
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.");
}
}
}
}
Nu när vi har denna värdtjänst, kan vi dramatiskt förbättra prestandan genom att skicka händelserna i bakgrunden.
Jag har använt detta på ett par olika ställen, på mina Program.cs
Jag bestämde mig för att experimentera med att spåra RSS-flöde begäran med Middleware, det bara upptäcker alla sökvägar som slutar i 'RSS' och skickar en bakgrundshändelse.
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();
});
Jag har också passerat mer data från min TranslateAPI
ändpunkt.
Vilket gör att jag kan se hur länge översättningar tar; notera att ingen av dessa blockerar huvudtråden ELLER spårning enskilda användare.
await umamiClient.SendBackground(new UmamiPayload(){ Name = "Get Translation"}, new UmamiEventData(){{"timetaken", translationTask.TotalMilliseconds}, {"language",translationTask.Language}});
var result = new TranslateResultTask(translationTask, true);
Umami API är lite udda men det är ett bra sätt att spåra händelser på ett självupptaget sätt. Förhoppningsvis får jag en chans att städa upp det ännu mer och få ett Umami nugget paket där ute. Dessutom från en tidigare artikel Jag vill dra data tillbaka från Umami för att ge funktioner som popularitet sortering.