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 einem früherer Posten Ich habe skizziert, wie ein Tracking Client für Umami in C# funktionieren könnte.
Nun, ich hatte endlich eine Chance, es ausgiebig zu testen und zu verbessern, es ist Betrieb (ja ANOTHER IHostedService
).
Die Umami Tracking API ist sowohl sehr oppositioniert als auch sehr knapp. Also musste ich den Client-Code aktualisieren, um folgendes zu handhaben:
Der Node API Client insgesamt ist unten, es ist super flexibel, aber WIRKLICH nicht gut dokumentiert.
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;
Wie Sie sehen, entlarven Sie die folgenden Methoden:
init
- Um die Optionen zu setzen.send
- Um die Nutzlast zu senden.track
- Um ein Ereignis zu verfolgen.identify
- Um einen Benutzer zu identifizieren.reset
- Um die Eigenschaften zurückzusetzen.Der Kern von diesem ist die send
Methode, die die Nutzlast an die API sendet.
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 }),
});
}
Um mit zu beginnen, kopierte ich ziemlich viel die Node API Clients UmamiOptions
und UmamiPayload
Unterricht (Ich werde nicht an ihnen wieder vorbei, sie sind groß).
So jetzt meine Send
Methode sieht so aus:
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;
}
Hier gibt es zwei kritische Teile:
PopulateFromPayload
Methode, die die Nutzlast mit der Website Id und der eventData bevölkert.PopulateFromPayload
Verfahren 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;
}
Sie können sehen, dass wir immer sicherstellen, dass die websiteId
ist gesetzt und wir setzen die anderen Werte nur, wenn sie nicht null sind. Das gibt uns Flexibilität auf Kosten einer gewissen Verbosität.
Wie bereits erwähnt, müssen wir der API einen etwas realen User-Agent String geben. Dies geschieht in der HttpClient
Einrichtung.
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
Das ist noch eine andere. IHostedService
, es gibt eine Reihe von Artikeln, wie man diese aufstellt, so dass ich nicht in sie hier gehen ( versuchen Sie die Suchleiste!)== Einzelnachweise ==
Der einzige Schmerzpunkt war die Anwendung der injizierten HttpClient
in der UmamiClient
Unterricht. Durch das Scoping des Clients und des Services habe ich einen IServiceScopeFactory
in den Konstrukteur des HostedService eingespritzt dann greifen Sie es für jede Anfrage senden.
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.");
}
}
}
}
Jetzt, da wir diesen gehosteten Service haben, können wir die Leistung drastisch verbessern, indem wir die Ereignisse im Hintergrund senden.
Ich habe dies an einigen verschiedenen Orten, in meinem Program.cs
Ich entschied mich, mit der Verfolgung der RSS-Feed-Anfrage mit Middleware zu experimentieren, es erkennt einfach jeden Pfad, der in 'RSS' endet und sendet ein Hintergrundereignis.
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();
});
Ich habe auch mehr Daten aus meinem TranslateAPI
Endpunkt.
Was mir erlaubt zu sehen, wie lange Übersetzungen nehmen; beachten Sie, dass keines davon den Hauptthread oder das Tracking einzelner Benutzer blockiert.
await umamiClient.SendBackground(new UmamiPayload(){ Name = "Get Translation"}, new UmamiEventData(){{"timetaken", translationTask.TotalMilliseconds}, {"language",translationTask.Language}});
var result = new TranslateResultTask(translationTask, true);
Die Umami API ist etwas skurril, aber es ist eine großartige Möglichkeit, Ereignisse auf eine selbstgehostete Weise zu verfolgen. Hoffentlich bekomme ich eine Chance, es noch mehr aufzuräumen und ein Umami Nuget-Paket da draußen zu bekommen. Darüber hinaus von einem früherer Artikel Ich möchte Daten zurück aus Umami ziehen, um Funktionen wie Popularität Sortierung zur Verfügung zu stellen.