Plausible Analytics

This commit is contained in:
Kieran 2022-09-07 12:40:52 +01:00
parent 1d451aac82
commit c3dbecca2a
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
8 changed files with 148 additions and 0 deletions

View File

@ -236,4 +236,7 @@ public static class Extensions
public static bool HasVirusScanner(this VoidSettings settings) public static bool HasVirusScanner(this VoidSettings settings)
=> settings.VirusScanner?.ClamAV != default || settings.VirusScanner?.VirusTotal != default; => settings.VirusScanner?.ClamAV != default || settings.VirusScanner?.VirusTotal != default;
public static bool HasPlausible(this VoidSettings settings)
=> settings.PlausibleAnalytics?.Endpoint != null;
} }

View File

@ -90,6 +90,11 @@ namespace VoidCat.Model
/// Select which store to use for files storage, if not set "local-disk" will be used /// Select which store to use for files storage, if not set "local-disk" will be used
/// </summary> /// </summary>
public string DefaultFileStore { get; init; } = "local-disk"; public string DefaultFileStore { get; init; } = "local-disk";
/// <summary>
/// Plausible Analytics endpoint url
/// </summary>
public PlausibleSettings? PlausibleAnalytics { get; init; }
} }
public sealed class TorSettings public sealed class TorSettings
@ -158,4 +163,10 @@ namespace VoidCat.Model
public Uri? Url { get; init; } public Uri? Url { get; init; }
public string? EgressQuery { get; init; } public string? EgressQuery { get; init; }
} }
public sealed class PlausibleSettings
{
public Uri? Endpoint { get; init; }
public string? Domain { get; init; }
}
} }

View File

@ -2,6 +2,7 @@ using Newtonsoft.Json;
using Prometheus; using Prometheus;
using VoidCat; using VoidCat;
using VoidCat.Model; using VoidCat.Model;
using VoidCat.Services.Analytics;
using VoidCat.Services.Migrations; using VoidCat.Services.Migrations;
JsonConvert.DefaultSettings = () => VoidStartup.ConfigJsonSettings(new()); JsonConvert.DefaultSettings = () => VoidStartup.ConfigJsonSettings(new());
@ -90,6 +91,7 @@ if (mode.HasFlag(RunModes.Webserver))
app.UseHealthChecks("/healthz"); app.UseHealthChecks("/healthz");
app.UseMiddleware<AnalyticsMiddleware>();
app.UseEndpoints(ep => app.UseEndpoints(ep =>
{ {
ep.MapControllers(); ep.MapControllers();

View File

@ -0,0 +1,6 @@
namespace VoidCat.Services.Abstractions;
public interface IWebAnalyticsCollector
{
Task TrackPageView(HttpContext context);
}

View File

@ -0,0 +1,32 @@
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Analytics;
public class AnalyticsMiddleware : IMiddleware
{
private readonly ILogger<AnalyticsMiddleware> _logger;
private readonly IEnumerable<IWebAnalyticsCollector> _collectors;
public AnalyticsMiddleware(IEnumerable<IWebAnalyticsCollector> collectors, ILogger<AnalyticsMiddleware> logger)
{
_collectors = collectors;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
foreach (var collector in _collectors)
{
try
{
await collector.TrackPageView(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to track page view");
}
}
await next(context);
}
}

View File

@ -0,0 +1,21 @@
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Analytics;
public static class AnalyticsStartup
{
/// <summary>
/// Add services needed to collect analytics
/// </summary>
/// <param name="services"></param>
/// <param name="settings"></param>
public static void AddAnalytics(this IServiceCollection services, VoidSettings settings)
{
services.AddTransient<AnalyticsMiddleware>();
if (settings.HasPlausible())
{
services.AddTransient<IWebAnalyticsCollector, PlausibleAnalytics>();
}
}
}

View File

@ -0,0 +1,71 @@
using System.Text;
using Newtonsoft.Json;
using VoidCat.Model;
using VoidCat.Services.Abstractions;
namespace VoidCat.Services.Analytics;
public class PlausibleAnalytics : IWebAnalyticsCollector
{
private readonly HttpClient _client;
private readonly string _domain;
public PlausibleAnalytics(HttpClient client, VoidSettings settings)
{
_client = client;
_client.BaseAddress = settings.PlausibleAnalytics!.Endpoint!;
_domain = settings.PlausibleAnalytics!.Domain!;
}
public async Task TrackPageView(HttpContext context)
{
var request = new HttpRequestMessage(HttpMethod.Post, "/api/event");
request.Headers.Add("user-agent", context.Request.Headers.UserAgent.First());
request.Headers.Add("x-forwarded-for",
context.Request.Headers.TryGetValue("x-forwarded-for", out var xff) ? xff.First() : null);
var ub = new UriBuilder("http:", context.Request.Host.Host, context.Request.Host.Port ?? 80,
context.Request.Path)
{
Query = context.Request.QueryString.Value
};
var ev = new EventObj(_domain, ub.Uri)
{
Referrer =
context.Request.Headers.Referer.Any()
? new Uri(context.Request.Headers.Referer.FirstOrDefault()!)
: null
};
request.Content = new ByteArrayContent(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(ev)));
request.Content.Headers.ContentType = new("application/json");
var rsp = await _client.SendAsync(request);
if (!rsp.IsSuccessStatusCode)
{
throw new Exception(
$"Invalid plausible analytics response {rsp.StatusCode} {await rsp.Content.ReadAsStringAsync()}");
}
}
internal class EventObj
{
public EventObj(string domain, Uri url)
{
Domain = domain;
Url = url;
}
[JsonProperty("name")] public string Name { get; init; } = "pageview";
[JsonProperty("domain")] public string Domain { get; init; }
[JsonProperty("url")] public Uri Url { get; init; }
[JsonProperty("screen_width")] public int? ScreenWidth { get; init; }
[JsonProperty("referrer")] public Uri? Referrer { get; init; }
[JsonProperty("props")] public object? Props { get; init; }
}
}

View File

@ -12,6 +12,7 @@ using StackExchange.Redis;
using VoidCat.Model; using VoidCat.Model;
using VoidCat.Services; using VoidCat.Services;
using VoidCat.Services.Abstractions; using VoidCat.Services.Abstractions;
using VoidCat.Services.Analytics;
using VoidCat.Services.Background; using VoidCat.Services.Background;
using VoidCat.Services.Captcha; using VoidCat.Services.Captcha;
using VoidCat.Services.Files; using VoidCat.Services.Files;
@ -150,6 +151,7 @@ public static class VoidStartup
}); });
services.AddTransient<RazorPartialToStringRenderer>(); services.AddTransient<RazorPartialToStringRenderer>();
services.AddAnalytics(voidSettings);
} }
public static void AddBackgroundServices(this IServiceCollection services, VoidSettings voidSettings) public static void AddBackgroundServices(this IServiceCollection services, VoidSettings voidSettings)