// Copyright Epic Games, Inc. All Rights Reserved. #include "MetaHumanCharacterAnalytics.h" #include "MetaHumanCharacter.h" #include "MetaHumanCharacterBodyIdentity.h" #include "MetaHumanCharacterEditorSubsystem.h" #include "MetaHumanCharacterPipeline.h" #include "Cloud/MetaHumanARServiceRequest.h" #include "EngineAnalytics.h" #include "Logging/LogMacros.h" #include "Logging/StructuredLog.h" namespace UE::MetaHuman::Analytics { namespace { const FString EventNamePrefix = TEXT("Editor.MetaHumanCharacter."); FString AnonymizeString(const FString& String) { FSHA1 Sha1; Sha1.UpdateWithString(*String, String.Len()); const FSHAHash HashedName = Sha1.Finalize(); return HashedName.ToString(); } FString AnonymizeName(const FName& Name) { return AnonymizeString(Name.ToString()); } void StartRecordEvent(TArray& EventAttributes, const UMetaHumanCharacter* InMetaHumanCharacter) { FPrimaryAssetId PrimaryAssetId(InMetaHumanCharacter->GetClass()->GetFName(), InMetaHumanCharacter->GetFName()); const FString PrimaryAssetIdStr = PrimaryAssetId.PrimaryAssetType.GetName().ToString() / PrimaryAssetId.PrimaryAssetName.ToString(); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("CharacterId"), AnonymizeString(PrimaryAssetId.PrimaryAssetType.GetName().ToString() / PrimaryAssetId.PrimaryAssetName.ToString()))); } void FinishRecordEvent(const FString& EventName, const TArray& EventAttributes) { check(FEngineAnalytics::IsAvailable()); const FString FullEventName = EventNamePrefix + EventName; FEngineAnalytics::GetProvider().RecordEvent(FullEventName, EventAttributes); } void RecordBodyTypeInformation(TArray& EventAttributes, const UMetaHumanCharacter* InMetaHumanCharacter) { if (UMetaHumanCharacterEditorSubsystem* MetaHumanCharacterEditorSubsystem = UMetaHumanCharacterEditorSubsystem::Get()) { TSharedRef BodyState = MetaHumanCharacterEditorSubsystem->GetBodyState(InMetaHumanCharacter); EMetaHumanBodyType BodyType = BodyState->GetMetaHumanBodyType(); if (BodyType == EMetaHumanBodyType::BlendableBody) { EventAttributes.Add(FAnalyticsEventAttribute(TEXT("BlendableBody"), true)); } else { EventAttributes.Add(FAnalyticsEventAttribute(TEXT("LegacyBodyType"), static_cast(BodyType))); } } } } #define NO_ANALYTICS_CIRCUIT_BREAK()\ if (!FEngineAnalytics::IsAvailable()) return #define BEGIN_RECORD_EVENT(EventName,FuncName,...)\ void Record##FuncName##Event(TNotNull InMetaHumanCharacter, __VA_ARGS__)\ {\ const FString EventNameStr = TEXT(#EventName);\ if (!FEngineAnalytics::IsAvailable()) return;\ TArray EventAttributes;\ StartRecordEvent(EventAttributes, InMetaHumanCharacter); #define END_RECORD_EVENT()\ FinishRecordEvent(EventNameStr, EventAttributes);\ } #define DEFINE_RECORD_EVENT(EventName,FuncName)\ void Record##FuncName##Event(TNotNull InMetaHumanCharacter)\ {\ const FString EventNameStr = TEXT(#EventName);\ if (!FEngineAnalytics::IsAvailable()) return;\ TArray EventAttributes;\ StartRecordEvent(EventAttributes, InMetaHumanCharacter);\ FinishRecordEvent(EventNameStr, EventAttributes);\ } DEFINE_RECORD_EVENT(New, NewCharacter); DEFINE_RECORD_EVENT(OpenEditor, OpenCharacterEditor); BEGIN_RECORD_EVENT(Build, BuildPipelineCharacter, const TSubclassOf InMaybePipeline) { if (InMaybePipeline != nullptr) { EventAttributes.Add(FAnalyticsEventAttribute(TEXT("PipelineID"), AnonymizeString(InMaybePipeline->GetPathName()))); } EventAttributes.Add(FAnalyticsEventAttribute(TEXT("HasSynthesisedTextures"), InMetaHumanCharacter->HasSynthesizedTextures())); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("HasHighResolutionTextures"), InMetaHumanCharacter->HasHighResolutionTextures())); RecordBodyTypeInformation(EventAttributes, InMetaHumanCharacter); } END_RECORD_EVENT(); BEGIN_RECORD_EVENT(Autorig, RequestAutorig, UE::MetaHuman::ERigType RigType) { EventAttributes.Add(FAnalyticsEventAttribute(TEXT("RigType"), static_cast(RigType))); RecordBodyTypeInformation(EventAttributes, InMetaHumanCharacter); } END_RECORD_EVENT(); BEGIN_RECORD_EVENT(HighResolutionTextures, RequestHighResolutionTextures, ERequestTextureResolution RequestTextureResolution) { EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Resolution"), static_cast(RequestTextureResolution))); } END_RECORD_EVENT(); DEFINE_RECORD_EVENT(SaveFaceDNA, SaveFaceDNA); DEFINE_RECORD_EVENT(SaveBodyDNA, SaveBodyDNA); DEFINE_RECORD_EVENT(SaveHighResolutionTextures, SaveHighResolutionTextures); DEFINE_RECORD_EVENT(ImportFaceDNA, ImportFaceDNA); DEFINE_RECORD_EVENT(ImportBodyDNA, ImportBodyDNA); DEFINE_RECORD_EVENT(CreateMeshFromDNA, CreateMeshFromDNA); void RecordWardrobeItemEventImpl(TArray& EventAttributes, const FName& SlotName, const FName& AssetName) { EventAttributes.Add(FAnalyticsEventAttribute(TEXT("AssetName"), AnonymizeName(AssetName))); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("SlotName"), SlotName.ToString())); //< this doesn't need to be anonymized since it's something *we* have defined } void RecordWardrobeItemWornEvent(const FName& SlotName, const FName& AssetName) { NO_ANALYTICS_CIRCUIT_BREAK(); TArray EventAttributes; RecordWardrobeItemEventImpl(EventAttributes, SlotName, AssetName); FinishRecordEvent(TEXT("WardrobeItemWorn"), EventAttributes); } void RecordWardrobeItemPreparedEvent(const FName& SlotName, const FName& AssetName) { NO_ANALYTICS_CIRCUIT_BREAK(); TArray EventAttributes; RecordWardrobeItemEventImpl(EventAttributes, SlotName, AssetName); FinishRecordEvent(TEXT("WardrobeItemPrepared"), EventAttributes); } }