#include "Core/TelemetrySubsystem.h" #include "Salty.h" #include "SaltyCore.h" #include "Telemetry/TelemetryEvents.h" #include "Telemetry/TelemetrySink.h" #include "Engine/GameInstance.h" #include "Engine/World.h" #include "GameFramework/GameStateBase.h" #include "HAL/IConsoleManager.h" #include "Misc/App.h" #include "Misc/CommandLine.h" #include "Misc/Paths.h" #ifndef SALTY_GIT_HASH #define SALTY_GIT_HASH "nogit" #endif namespace { // telemetry.File 1 installs the file sink on the next game instance (set it in the editor console before // Play In Editor to get one file per PIE instance); -telemetry on the command line does the same. TAutoConsoleVariable CVarTelemetryFile( TEXT("telemetry.File"), 0, TEXT("1: write telemetry to Saved/Telemetry/session_*.jsonl. Read when a game instance starts."), ECVF_Default); } void UTelemetrySubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); BuildString = FString::Printf(TEXT("%s+%s"), FApp::GetBuildVersion(), TEXT(SALTY_GIT_HASH)); Sink = MakeDefaultSink(); Emit(TelemetryEvents::AppStarted, FTelemetryPayload().Set(TEXT("engine"), FEngineVersion::Current().ToString())); } void UTelemetrySubsystem::Deinitialize() { if (HasSession()) { Emit(TelemetryEvents::SessionEnded); } if (Sink) { Sink->Flush(); Sink.Reset(); } Super::Deinitialize(); } TUniquePtr UTelemetrySubsystem::MakeDefaultSink() const { const bool bFileRequested = FParse::Param(FCommandLine::Get(), TEXT("telemetry")) || CVarTelemetryFile.GetValueOnGameThread() != 0; if (bFileRequested) { // One file per peer; the instance id keeps two PIE instances started in the same second apart. const FString FileName = FString::Printf(TEXT("session_%s_%s.jsonl"), *FDateTime::UtcNow().ToString(TEXT("%Y%m%d_%H%M%S")), *FGuid::NewGuid().ToString(EGuidFormats::Digits).Left(8)); const FString Path = FPaths::ProjectSavedDir() / TEXT("Telemetry") / FileName; UE_LOG(LogTelemetry, Log, TEXT("Telemetry file sink: %s"), *Path); return MakeUnique(Path); } #if WITH_EDITOR if (GIsEditor) { return MakeUnique(); } #endif return MakeUnique(); } void UTelemetrySubsystem::SetSink(TUniquePtr InSink) { if (Sink) { Sink->Flush(); } Sink = MoveTemp(InSink); if (!Sink) { Sink = MakeUnique(); } } void UTelemetrySubsystem::Emit(FName Name, TSharedPtr Payload) { if (!Sink) { return; } FTelemetryEvent Event; Event.Name = Name; Event.Payload = MoveTemp(Payload); Sink->Emit(StampEnvelope(), Event); } void UTelemetrySubsystem::BeginSession(FGuid InSessionId) { if (!InSessionId.IsValid() || InSessionId == SessionId) { return; } SessionId = InSessionId; Emit(TelemetryEvents::SessionStarted); } void UTelemetrySubsystem::MarkCheatUsed(FName Command) { bCheatsUsed = true; Emit(TelemetryEvents::CheatUsed, FTelemetryPayload().Set(TEXT("command"), Command)); } FTelemetryEnvelope UTelemetrySubsystem::StampEnvelope() const { FTelemetryEnvelope Envelope; Envelope.EventId = FGuid::NewGuid(); Envelope.TimestampUtc = FDateTime::UtcNow(); Envelope.SessionId = SessionId; Envelope.Build = BuildString; Envelope.bCheatsUsed = bCheatsUsed; // PlayerId stays empty until step 3 mints one on the player state (Docs/Spec/Networking.md, Identity). const UGameInstance* GameInstance = GetGameInstance(); const UWorld* World = GameInstance ? GameInstance->GetWorld() : nullptr; if (World) { Envelope.GameTime = World->GetTimeSeconds(); Envelope.bIsServer = World->GetNetMode() != NM_Client; // standalone is its own authority Envelope.Map = UWorld::RemovePIEPrefix(World->GetMapName()); if (const AGameStateBase* GameState = World->GetGameState()) { Envelope.PartySize = GameState->PlayerArray.Num(); } } return Envelope; }