Files
UnrealEngineUWP/Engine/Source/Runtime/Engine/Private/EngineAnalytics.cpp
michael atchison 2be2924064 Fix usage of SendMachineInfoForAccount.
Add comment about sending SessionMachineInfo with a blank account id.

[REVIEW] [at]rafa.lecina, [at]eric.day
#preflight 63f95302dd78dd50f6b57f76

[CL 24426228 by michael atchison in ue5-main branch]
2023-02-27 12:25:13 -05:00

348 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "EngineAnalytics.h"
#include "Misc/App.h"
#include "Stats/Stats.h"
#include "Misc/ConfigCacheIni.h"
#include "Engine/Engine.h"
#include "Misc/EngineBuildSettings.h"
#include "AnalyticsBuildType.h"
#include "IAnalyticsProviderET.h"
#include "GeneralProjectSettings.h"
#include "Misc/EngineVersion.h"
#include "BuildSettings.h"
#include "RHI.h"
#include "GenericPlatform/GenericPlatformCrashContext.h"
#include "StudioAnalytics.h"
#include "UObject/Class.h"
#include "Containers/Set.h"
#if WITH_EDITOR
#include "AnalyticsSessionSummaryManager.h"
#include "AnalyticsSessionSummarySender.h"
#include "Analytics/EditorAnalyticsSessionSummary.h"
#include "EditorAnalyticsSession.h" // DEPRECATED: kept around to clean up expired old sessions.
#include "Horde.h"
#endif
bool FEngineAnalytics::bIsInitialized;
TSharedPtr<IAnalyticsProviderET> FEngineAnalytics::Analytics;
TSet<FString> FEngineAnalytics::SessionEpicAccountIds;
#if WITH_EDITOR
static TUniquePtr<FAnalyticsSessionSummaryManager> AnalyticsSessionSummaryManager;
static TUniquePtr<FEditorAnalyticsSessionSummary> EditorAnalyticSessionSummary;
static TSharedPtr<FAnalyticsSessionSummarySender> AnalyticsSessionSummarySender;
FSimpleMulticastDelegate FEngineAnalytics::OnInitializeEngineAnalytics;
FSimpleMulticastDelegate FEngineAnalytics::OnShutdownEngineAnalytics;
#endif
namespace UE::Analytics::Private
{
IEngineAnalyticsConfigOverride* EngineAnalyticsConfigOverride = nullptr;
}
static TSharedPtr<IAnalyticsProviderET> CreateEpicAnalyticsProvider()
{
FAnalyticsET::Config Config;
{
// We always use the "Release" analytics account unless we're running in analytics test mode (usually with
// a command-line parameter), or we're an internal Epic build
const EAnalyticsBuildType AnalyticsBuildType = GetAnalyticsBuildType();
const bool bUseReleaseAccount =
(AnalyticsBuildType == EAnalyticsBuildType::Development || AnalyticsBuildType == EAnalyticsBuildType::Release) &&
!FEngineBuildSettings::IsInternalBuild(); // Internal Epic build
const TCHAR* BuildTypeStr = bUseReleaseAccount ? TEXT("Release") : TEXT("Dev");
FString UETypeOverride;
bool bHasOverride = GConfig->GetString(TEXT("Analytics"), TEXT("UE4TypeOverride"), UETypeOverride, GEngineIni);
const TCHAR* UETypeStr = bHasOverride ? *UETypeOverride : FEngineBuildSettings::IsPerforceBuild() ? TEXT("Perforce") : TEXT("UnrealEngine");
FString AppID;
GConfig->GetString(TEXT("Analytics"), TEXT("AppIdOverride"), AppID, GEditorIni);
Config.APIKeyET = FString::Printf(TEXT("%s.%s.%s"), AppID.IsEmpty() ? TEXT("UEEditor") : *AppID, UETypeStr, BuildTypeStr);
}
Config.APIServerET = TEXT("https://datarouter.ol.epicgames.com/");
Config.AppEnvironment = TEXT("datacollector-binary");
Config.AppVersionET = FEngineVersion::Current().ToString();
if (UE::Analytics::Private::EngineAnalyticsConfigOverride)
{
UE::Analytics::Private::EngineAnalyticsConfigOverride->ApplyConfiguration(Config);
}
// Connect the engine analytics provider (if there is a configuration delegate installed)
return FAnalyticsET::Get().CreateAnalyticsProvider(Config);
}
FString CreateAnalyticsUserId(const FString& EpicAccountId)
{
return FString::Printf(TEXT("%s|%s|%s"), *FPlatformMisc::GetLoginId(), *EpicAccountId, *FPlatformMisc::GetOperatingSystemId());
}
IAnalyticsProviderET& FEngineAnalytics::GetProvider()
{
checkf(bIsInitialized && IsAvailable(), TEXT("FEngineAnalytics::GetProvider called outside of Initialize/Shutdown."));
return *Analytics.Get();
}
#if WITH_EDITOR
FAnalyticsSessionSummaryManager& FEngineAnalytics::GetSummaryManager()
{
checkf(bIsInitialized && AnalyticsSessionSummaryManager.IsValid(), TEXT("FEngineAnalytics::GetSessionManager called outside of Initialize/Shutdown."));
return *AnalyticsSessionSummaryManager.Get();
}
#endif
void FEngineAnalytics::Initialize()
{
checkf(!bIsInitialized, TEXT("FEngineAnalytics::Initialize called more than once."));
check(GEngine);
#if WITH_EDITOR
// this will only be true for builds that have editor support (desktop platforms)
// The idea here is to only send editor events for actual editor runs, not for things like -game runs of the editor.
bool bIsEditorRun = GIsEditor && !IsRunningCommandlet();
#else
bool bIsEditorRun = false;
#endif
#if UE_BUILD_DEBUG
const bool bShouldInitAnalytics = false;
#else
// Outside of the editor, the only engine analytics usage is the hardware survey
const bool bShouldInitAnalytics = bIsEditorRun && GEngine->AreEditorAnalyticsEnabled();
#endif
if (bShouldInitAnalytics)
{
Analytics = CreateEpicAnalyticsProvider();
if (Analytics.IsValid())
{
Analytics->SetUserID(CreateAnalyticsUserId(FPlatformMisc::GetEpicAccountId()));
if (UE::Analytics::Private::EngineAnalyticsConfigOverride)
{
UE::Analytics::Private::EngineAnalyticsConfigOverride->OnInitialized(*Analytics, UE::Analytics::Private::FOnEpicAccountIdChanged::CreateStatic(&FEngineAnalytics::OnEpicAccountIdChanged));
}
TArray<FAnalyticsEventAttribute> StartSessionAttributes;
AppendMachineStats(StartSessionAttributes);
// allow editor events to be correlated to StudioAnalytics events (if there is a studio analytics provider)
if (FStudioAnalytics::IsAvailable())
{
Analytics->SetDefaultEventAttributes(MakeAnalyticsEventAttributeArray(TEXT("StudioAnalyticsSessionID"), FStudioAnalytics::GetProvider().GetSessionID()));
}
Analytics->StartSession(MoveTemp(StartSessionAttributes));
SendMachineInfoForAccount(FPlatformMisc::GetEpicAccountId());
bIsInitialized = true;
}
#if WITH_EDITOR
if (!AnalyticsSessionSummaryManager)
{
// Create the session summary manager for the Editor instance.
AnalyticsSessionSummaryManager = MakeUnique<FAnalyticsSessionSummaryManager>(
TEXT("Editor"), // The tag name of the process.
FApp::GetInstanceId().ToString(EGuidFormats::Digits), // Unique key to link the principal process (Editor) with subsidiary processes (CRC), that key is passed to CRC.
Analytics->GetUserID(),
Analytics->GetAppID(),
Analytics->GetAppVersion(),
Analytics->GetSessionID());
// The sender will sends orphans sessions and maybe this session if CRC dies first.
AnalyticsSessionSummaryManager->SetSender(MakeShared<FAnalyticsSessionSummarySender>(FEngineAnalytics::GetProvider()));
// Create a property store file with enough pre-reserved capacity to store the analytics data. This reduce risk of running out of disk later.
constexpr uint32 ReservedFileCapacity = 16 * 1024;
if (TSharedPtr<IAnalyticsPropertyStore> EditorPropertyStore = AnalyticsSessionSummaryManager->MakeStore(ReservedFileCapacity))
{
// Create the object responsible to collect the Editor session properties.
EditorAnalyticSessionSummary = MakeUnique<FEditorAnalyticsSessionSummary>(EditorPropertyStore, FGenericCrashContext::GetOutOfProcessCrashReporterProcessId());
}
OnInitializeEngineAnalytics.Broadcast();
}
#endif
}
}
void FEngineAnalytics::Shutdown(bool bIsEngineShutdown)
{
#if WITH_EDITOR
OnShutdownEngineAnalytics.Broadcast();
if (EditorAnalyticSessionSummary)
{
EditorAnalyticSessionSummary->Shutdown();
EditorAnalyticSessionSummary.Reset();
}
if (AnalyticsSessionSummaryManager)
{
bool bDiscard = !bIsEngineShutdown; // User toggled the 'Send Data' off.
AnalyticsSessionSummaryManager->Shutdown(bDiscard);
AnalyticsSessionSummaryManager.Reset();
}
else
{
// The manager cleans any left-over (crash, power outage) on shutdown when analytics is on but if off, ensure to clean up what could be left from when it was on.
FAnalyticsSessionSummaryManager::CleanupExpiredFiles();
}
// Clean up the outdated sessions created by the deprecated system. If an older compabile Editor is launched, it can still send them up before they get expired.
CleanupDeprecatedAnalyticSessions(FAnalyticsSessionSummaryManager::GetSessionExpirationAge());
#endif
bIsInitialized = false;
ensure(!Analytics.IsValid() || Analytics.IsUnique());
Analytics.Reset();
}
void FEngineAnalytics::Tick(float DeltaTime)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FEngineAnalytics_Tick);
#if WITH_EDITOR
if (EditorAnalyticSessionSummary)
{
EditorAnalyticSessionSummary->Tick(DeltaTime);
}
if (AnalyticsSessionSummaryManager.IsValid())
{
AnalyticsSessionSummaryManager->Tick();
}
#endif
}
void FEngineAnalytics::LowDriveSpaceDetected()
{
#if WITH_EDITOR
if (EditorAnalyticSessionSummary)
{
EditorAnalyticSessionSummary->LowDriveSpaceDetected();
}
#endif
}
void FEngineAnalytics::AppendMachineStats(TArray<FAnalyticsEventAttribute>& EventAttributes)
{
const FString UserID = FPlatformProcess::UserName(false);
const UGeneralProjectSettings& ProjectSettings = *GetDefault<UGeneralProjectSettings>();
const FPlatformMemoryStats Stats = FPlatformMemory::GetStats();
FString OSMajor;
FString OSMinor;
FPlatformMisc::GetOSVersions(/*out*/ OSMajor, /*out*/ OSMinor);
GEngine->CreateStartupAnalyticsAttributes( EventAttributes );
// Add project info whether we are in editor or game.
EventAttributes.Emplace(TEXT("ProjectName"), ProjectSettings.ProjectName);
EventAttributes.Emplace(TEXT("ProjectID"), ProjectSettings.ProjectID);
EventAttributes.Emplace(TEXT("ProjectDescription"), ProjectSettings.Description);
EventAttributes.Emplace(TEXT("ProjectVersion"), ProjectSettings.ProjectVersion);
EventAttributes.Emplace(TEXT("Application.Commandline"), FCommandLine::Get());
EventAttributes.Emplace(TEXT("User.ID"), UserID);
EventAttributes.Emplace(TEXT("Build.Configuration"), LexToString(FApp::GetBuildConfiguration()));
EventAttributes.Emplace(TEXT("Build.IsInternalBuild"), FEngineBuildSettings::IsInternalBuild());
EventAttributes.Emplace(TEXT("Build.IsPerforceBuild"), FEngineBuildSettings::IsPerforceBuild());
EventAttributes.Emplace(TEXT("Build.IsPromotedBuild"), FApp::GetEngineIsPromotedBuild() == 0 ? false : true);
EventAttributes.Emplace(TEXT("Build.BranchName"), FApp::GetBranchName());
EventAttributes.Emplace(TEXT("Build.Changelist"), BuildSettings::GetCurrentChangelist());
EventAttributes.Emplace(TEXT("Config.IsEditor"), GIsEditor);
EventAttributes.Emplace(TEXT("Config.IsUnattended"), FApp::IsUnattended());
EventAttributes.Emplace(TEXT("Config.IsBuildMachine"), GIsBuildMachine);
EventAttributes.Emplace(TEXT("Config.IsRunningCommandlet"), IsRunningCommandlet());
EventAttributes.Emplace(TEXT("Platform.IsRemoteSession"), FPlatformMisc::IsRemoteSession());
EventAttributes.Emplace(TEXT("OSMajor"), OSMajor);
EventAttributes.Emplace(TEXT("OSMinor"), OSMinor);
EventAttributes.Emplace(TEXT("OSVersion"), FPlatformMisc::GetOSVersion());
EventAttributes.Emplace(TEXT("Is64BitOS"), FPlatformMisc::Is64bitOperatingSystem());
EventAttributes.Emplace(TEXT("GPUVendorID"), GRHIVendorId);
EventAttributes.Emplace(TEXT("GPUDeviceID"), GRHIDeviceId);
EventAttributes.Emplace(TEXT("GRHIDeviceRevision"), GRHIDeviceRevision);
EventAttributes.Emplace(TEXT("GRHIAdapterInternalDriverVersion"), GRHIAdapterInternalDriverVersion);
EventAttributes.Emplace(TEXT("GRHIAdapterUserDriverVersion"), GRHIAdapterUserDriverVersion);
EventAttributes.Emplace(TEXT("TotalPhysicalRAM"), static_cast<uint64>(Stats.TotalPhysical));
EventAttributes.Emplace(TEXT("CPUPhysicalCores"), FPlatformMisc::NumberOfCores());
EventAttributes.Emplace(TEXT("CPULogicalCores"), FPlatformMisc::NumberOfCoresIncludingHyperthreads());
EventAttributes.Emplace(TEXT("DesktopGPUAdapter"), FPlatformMisc::GetPrimaryGPUBrand());
EventAttributes.Emplace(TEXT("RenderingGPUAdapter"), GRHIAdapterName);
EventAttributes.Emplace(TEXT("CPUVendor"), FPlatformMisc::GetCPUVendor());
EventAttributes.Emplace(TEXT("CPUBrand"), FPlatformMisc::GetCPUBrand());
#if WITH_EDITOR
EventAttributes.Emplace(TEXT("Horde.TemplateID"), FHorde::GetTemplateId());
EventAttributes.Emplace(TEXT("Horde.TemplateName"), FHorde::GetTemplateName());
EventAttributes.Emplace(TEXT("Horde.JobURL"), FHorde::GetJobURL());
EventAttributes.Emplace(TEXT("Horde.JobID"), FHorde::GetJobId());
EventAttributes.Emplace(TEXT("Horde.StepName"), FHorde::GetStepName());
EventAttributes.Emplace(TEXT("Horde.StepID"), FHorde::GetStepId());
EventAttributes.Emplace(TEXT("Horde.StepURL"), FHorde::GetStepURL());
EventAttributes.Emplace(TEXT("Horde.BatchID"), FHorde::GetBatchId());
#endif
#if PLATFORM_MAC
#if PLATFORM_MAC_ARM64
EventAttributes.Emplace(TEXT("UEBuildArch"), FString(TEXT("AppleSilicon")));
#else
EventAttributes.Emplace(TEXT("UEBuildArch"), FString(TEXT("Intel(Mac)")));
#endif
#endif
}
void FEngineAnalytics::SendMachineInfoForAccount(const FString& EpicAccountId)
{
// Note: EpicAccountId may be empty when the user has not signed in to the epic games launcher.
// The intention here is to only send SessionMachineStats once per unique user including when
// no user has logged in.
if (Analytics && !SessionEpicAccountIds.Contains(EpicAccountId))
{
SessionEpicAccountIds.Add(EpicAccountId);
TArray<FAnalyticsEventAttribute> EventAttributes;
AppendMachineStats(EventAttributes);
// When the user id is changed send a SessionMachineStats event.
static const FString SZEventName = TEXT("SessionMachineStats");
Analytics->RecordEvent(SZEventName, EventAttributes);
}
}
void FEngineAnalytics::OnEpicAccountIdChanged(const FString& EpicAccountId)
{
// For analytics reporting ignore changes to an empty account id when the user logs out.
if (!EpicAccountId.IsEmpty())
{
const FString NewAnalyticsUserId = CreateAnalyticsUserId(EpicAccountId);
// Update analytics provider user.
if (Analytics)
{
Analytics->SetUserID(NewAnalyticsUserId);
SendMachineInfoForAccount(EpicAccountId);
}
#if WITH_EDITOR
// Update the summary manager and all of the data stores.
if (AnalyticsSessionSummaryManager)
{
AnalyticsSessionSummaryManager->SetUserId(NewAnalyticsUserId);
}
#endif
// Update the crash context so the user id will be sent with runtime events to CRCEditor.
FGenericCrashContext::SetEpicAccountId(EpicAccountId);
}
}