Code/Terrywatch.cs
using Sandbox;

public static class TerryWatch
	{
		public static bool Debug = false;
		public static bool NeedsReconnect = false;

		private static string APIKEY;
		private static string APPID;
		private static string SessionToken;

		public static void Init( string apiKey = "", string appId = "", bool debug = false )
		{
			APIKEY = apiKey;
			APPID = appId;
			Debug = debug;
		}

		public static async void StartSession( string steamId, string authToken )
		{
			NeedsReconnect = false;

			var content = Http.CreateJsonContent( new { steam_id = steamId, auth_token = authToken } );

			var headers = new System.Collections.Generic.Dictionary<string, string>
			{
				{ "Authorization", $"Bearer {APIKEY}" },
				{ "X-App-Id", APPID },
				{ "Accept", "application/json" }
			};

			var response = await Http.RequestAsync( "https://terrywatch.com/api/sessions/start", "POST", content, headers );
			var body = await response.Content.ReadAsStringAsync();

			if ( Debug ) Log.Info( $"[TerryWatch] StartSession {(int)response.StatusCode}: {body}" );

			SessionToken = ExtractJsonValue( body, "session_token" );
			Log.Info( $"[TerryWatch] Session started: {ExtractJsonValue( body, "session_id" )}" );
		}

		public static async void SendEvent( string name, object properties = null )
		{
			if ( string.IsNullOrEmpty( SessionToken ) )
			{
				Log.Warning( "[TerryWatch] No active session." );
				return;
			}

			var content = Http.CreateJsonContent( new { name, properties } );

			var headers = new System.Collections.Generic.Dictionary<string, string>
			{
				{ "Authorization", $"Bearer {SessionToken}" },
				{ "Accept", "application/json" }
			};

			var response = await Http.RequestAsync( "https://terrywatch.com/api/events", "POST", content, headers );
			var body = await response.Content.ReadAsStringAsync();

			if ( Debug ) Log.Info( $"[TerryWatch] SendEvent {(int)response.StatusCode}: {body}" );

			if ( response.StatusCode == System.Net.HttpStatusCode.Unauthorized )
			{
				Log.Warning( "[TerryWatch] Session expired, reconnecting..." );
				SessionToken = null;
				NeedsReconnect = true;
			}
		}

		public static async void EndSession()
		{
			if ( string.IsNullOrEmpty( SessionToken ) ) return;

			var content = Http.CreateJsonContent( new {} );

			var headers = new System.Collections.Generic.Dictionary<string, string>
			{
				{ "Authorization", $"Bearer {SessionToken}" },
				{ "Accept", "application/json" }
			};

			var response = await Http.RequestAsync( "https://terrywatch.com/api/sessions/end", "POST", content, headers );
			var body = await response.Content.ReadAsStringAsync();

			if ( Debug ) Log.Info( $"[TerryWatch] EndSession {(int)response.StatusCode}: {body}" );
			Log.Info( "[TerryWatch] Session ended." );
			SessionToken = null;
		}

		private static string ExtractJsonValue( string json, string key )
		{
			var search = $"\"{key}\":\"";
			var start = json.IndexOf( search );
			if ( start == -1 ) return null;
			start += search.Length;
			var end = json.IndexOf( '"', start );
			return end == -1 ? null : json.Substring( start, end - start );
		}
}