Code/MetricsSystem.cs
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Text.Json;

public class MetricsSystem : GameObjectSystem
{
	[ConVar( "manic_metrics_enabled", Saved = true )]
	public static bool Enabled { get; set; } = true;
	[ConVar( "manic_metrics_endpoint", Saved = true )]
	public static string URL { get; set; } = "https://ggg.sethp.cc/api/v1/import";
	[ConVar( "manic_metrics_cooldown", Saved = true )]
	public static int MetricsDelay { get; set; } = 5;
	[ConVar( "manic_metrics_logging", Saved = true )]
	public static bool DebugLogging { get; set; } = false;
	TimeUntil canSendMetrics = MetricsDelay;
	public Dictionary<string, string> GzipHeader = new()
	{
		{"Content-Encoding", "gzip"}
	};
	public MetricsSystem( Scene scene ) : base( scene )
	{
		// Only the host send off metrics
		if ( !Networking.IsHost )
			return;

		if ( !Http.IsAllowed( new System.Uri( URL ) ) )
			throw new Exception( $"cannot send metrics to there, not allowed: {URL}" );

		// Listen( Stage.SceneLoaded, 1, DebugMetrics, "DebugMetrics" );
		Listen( Stage.FinishFixedUpdate, 1, SendMetrics, "SendMetrics" );
	}

	private readonly List<MetricStreamEntry> Queue = new();

	void DebugMetrics()
	{
		Log.Info( $"Prepping to send debug game metrics to: {URL}" );

		var m = new MetricStreamEntry()
		{
			Metric = new SampleMetric( "debug" )
		};

		var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
		m.Backfill( now - 1000 * 60 * 2, 2 );
		m.Backfill( now - 1000 * 60 * 1, 6 );
		m.Backfill( now - 1000 * 20, 8 );
		m.Add( 10 );

		Queue.Add( m );

		m = new MetricStreamEntry()
		{
			Metric = new SampleMetric( "debug2" )
		};

		m.Backfill( now - 1000 * 80 * 2, 1 );
		m.Backfill( now - 1000 * 40 * 1, 20 );
		m.Backfill( now - 1000 * 10, 40 );
		m.Add( 55 );

		Queue.Add( m );
	}

	void SendMetrics()
	{
		if ( Game.ActiveScene.IsEditor || !Enabled )
			return;

		if ( canSendMetrics && Queue.Count != 0 )
		{
			canSendMetrics = MetricsDelay;
			FlushMetrics();
		}
	}

	static readonly bool isUsingGzip = false;

	async void FlushMetrics()
	{
		var body = string.Join( "\n", Queue.Select( mse => JsonSerializer.Serialize( mse ) ) );
		if ( DebugLogging )
		{
			Log.Warning( $"==== Metrics Payload ====" );
			Log.Warning( body );
		}

		HttpContent content;
		Dictionary<string, string> headers = null;

		if ( isUsingGzip )
		{
			byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes( body );
			using var outputStream = new MemoryStream();
			using ( var gZipStream = new GZipStream( outputStream, CompressionMode.Compress ) )
				gZipStream.Write( inputBytes, 0, inputBytes.Length );
			headers = GzipHeader;
			content = new ByteArrayContent( outputStream.ToArray() );
		}
		else
		{
			content = new StringContent( body );
		}

		var url = URL
			+ $"?extra_label=editor={Game.IsEditor}"
			+ $"&extra_label=ident={Game.Ident}"
			+ $"&extra_label=hoststeamid={Game.SteamId}"
			+ $"&extra_label=scene={Game.ActiveScene.Name}"
			+ $"&extra_label=party={Connection.Local.PartyId}";

		if ( Enabled )
		{
			var response = await Http.RequestAsync( url, "POST", content, headers );
			if ( !response.IsSuccessStatusCode )
				Log.Info( $"Metrics Upload Issue: {response.StatusCode} {response}" );

			if ( DebugLogging )
				Log.Info( $"Metrics Upload Status: {response.StatusCode} {response}" );
		}

		Queue.Clear();
	}

	private static MetricStreamEntry GetEntry( IMetric metric )
	{
		var system = Game.ActiveScene.GetSystem<MetricsSystem>();
		system.canSendMetrics = MetricsDelay; // re-up flush delay
		var streamEntry = system.Queue.FirstOrDefault( mse => mse.Metric.GetHashCode().Equals( metric.GetHashCode() ) );
		if ( streamEntry == null )
		{
			if ( DebugLogging )
				Log.Info( $"adding '{metric.Name}' to the send queue" );
			streamEntry = new MetricStreamEntry() { Metric = metric };
			system.Queue.Add( streamEntry );
		}
		return streamEntry;
	}

	#region Commands

	[ConCmd( "manic_metrics", Help = "print queued metrics" )]
	public static void Kill()
	{
		var system = Game.ActiveScene.GetSystem<MetricsSystem>();

		if ( system.Queue.Count == 0 )
		{
			Log.Info( "Metrics Queue is empty" );
			return;
		}

		Log.Info( $"==== Metrics in Queue: {system.Queue.Count} ====" );
		foreach ( var mqe in system.Queue )
			Log.Info( mqe );
	}

	#endregion

	#region Utilities

	public static void Increment( IMetric metric )
	{
		if ( !Networking.IsHost ) return;
		GetEntry( metric ).Increment();
	}

	public static void Set( IMetric metric, float value )
	{
		if ( !Networking.IsHost ) return;
		GetEntry( metric ).Set( value );
	}

	#endregion
}