Analytics/NetworkStorageAnalytics.cs
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
namespace Sandbox;
public static partial class NetworkStorage
{
public static Task TrackAnalyticsEvent( string eventType, object payload = null )
=> SendAnalyticsEvent( eventType, payload, "custom", null, null );
public static Task TrackAnalyticsWarning( string code, string message = null, object context = null )
=> AnalyticsCaptureWarnings
? SendAnalyticsEvent( code, context, "warning", message, null )
: Task.CompletedTask;
public static Task TrackAnalyticsError( Exception exception, string code = null, object context = null )
=> TrackAnalyticsError( code ?? exception?.GetType().Name ?? "error", exception?.Message, exception?.StackTrace, context );
public static Task TrackAnalyticsError( string code, string message = null, string stack = null, object context = null )
=> AnalyticsCaptureErrors
? SendAnalyticsEvent( code, context, "error", message, AnalyticsRetainErrorStacks ? stack : null )
: Task.CompletedTask;
public static Task TrackSessionStart( object context = null )
=> AnalyticsCaptureSessions ? SendSessionSignal( "join", 0, context, null, null ) : Task.CompletedTask;
public static Task TrackSessionHeartbeat( double sessionSeconds = 0, object context = null, object fps = null )
=> AnalyticsCaptureSessions ? SendSessionSignal( "heartbeat", sessionSeconds, context, fps, null ) : Task.CompletedTask;
public static Task TrackSessionEnd( double durationSeconds = 0, object context = null )
=> AnalyticsCaptureSessions ? SendSessionSignal( "leave", durationSeconds, context, null, null ) : Task.CompletedTask;
internal static Task TrackManagedSessionSignal( string eventType, string sessionId, double sessionSeconds, object context = null, object fps = null )
=> AnalyticsCaptureSessions ? SendSessionSignal( eventType, sessionSeconds, context, fps, sessionId ) : Task.CompletedTask;
internal static Task TrackManagedDiagnosticEvent( string eventType, object payload = null, string label = null )
=> EnablePlayerAnalytics ? SendAnalyticsEvent( eventType, payload, "info", label, null ) : Task.CompletedTask;
private static async Task SendSessionSignal( string eventType, double sessionSeconds, object context, object fps, string sessionId )
{
try
{
EnsureConfigured();
await EnsureRuntimeSecurityConfigAsync( "analytics-session" );
if ( !EnablePlayerAnalytics || !AnalyticsCaptureSessions ) return;
var body = new Dictionary<string, object>
{
["steamId"] = Game.SteamId.ToString(),
["sessionId"] = sessionId,
["sessionSeconds"] = Math.Max( 0, sessionSeconds ),
["event"] = eventType,
["playerName"] = Connection.Local?.DisplayName,
["source"] = "network-storage-library",
["libraryVersion"] = NetworkStorage.PackageVersion,
["packageIdent"] = NetworkStoragePackageInfo.PackageIdent,
["context"] = context,
["fps"] = fps
};
var headers = await BuildAuthHeaders();
var url = BuildUrl( $"/storage/{Uri.EscapeDataString( ProjectId )}/stats/heartbeat" );
var content = Http.CreateJsonContent( body );
await Http.RequestStringAsync( url, "POST", content, headers );
if ( NetworkStorageLogConfig.LogRequests )
{
var logLine = $"Reported session:{eventType} session={sessionId ?? "manual"} seconds={sessionSeconds:0}s";
NetLog.Info( "analytics", logLine );
Log.Info( $"[NetworkStorage] Analytics {logLine}" );
}
}
catch ( Exception ex )
{
if ( NetworkStorageLogConfig.LogErrors )
NetLog.Error( "analytics", $"Session analytics failed: {ex.Message}" );
}
}
private static async Task SendAnalyticsEvent( string eventType, object payload, string severity, string message, string stack )
{
try
{
EnsureConfigured();
await EnsureRuntimeSecurityConfigAsync( "analytics" );
if ( !EnablePlayerAnalytics ) return;
var normalized = NormalizeAnalyticsEventType( eventType );
if ( string.IsNullOrWhiteSpace( normalized ) ) return;
if ( severity == "custom" && !IsAnalyticsEventAllowed( normalized ) )
{
if ( NetworkStorageLogConfig.LogRequests )
NetLog.Info( "analytics", $"Suppressed disallowed event {normalized}" );
return;
}
var steamId = Game.SteamId.ToString();
var reportType = severity == "session" ? $"session.{normalized}" : normalized;
var body = new Dictionary<string, object>
{
["steamId"] = steamId,
["type"] = reportType,
["severity"] = severity == "custom" ? null : severity,
["label"] = severity == "info" && !string.IsNullOrWhiteSpace( message ) ? message : normalized,
["message"] = message,
["stack"] = stack,
["context"] = payload,
["source"] = "network-storage-library",
["libraryVersion"] = NetworkStorage.PackageVersion,
["packageIdent"] = NetworkStoragePackageInfo.PackageIdent
};
var headers = await BuildAuthHeaders();
var url = BuildUrl( $"/storage/{Uri.EscapeDataString( ProjectId )}/analytics/events" );
var content = Http.CreateJsonContent( body );
await Http.RequestStringAsync( url, "POST", content, headers );
if ( NetworkStorageLogConfig.LogRequests )
NetLog.Info( "analytics", $"Reported {severity}:{normalized}" );
}
catch ( Exception ex )
{
if ( NetworkStorageLogConfig.LogErrors )
NetLog.Error( "analytics", $"Analytics report failed: {ex.Message}" );
}
}
}