2021-05-13 21:58:20 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
# include "AnalyticsSessionSummaryManager.h"
# include "AnalyticsPropertyStore.h"
# include "IAnalyticsSessionSummarySender.h"
# include "GenericPlatform/GenericPlatformProcess.h"
# include "HAL/CriticalSection.h"
# include "HAL/FileManager.h"
# include "HAL/PlatformProcess.h"
# include "HAL/PlatformTime.h"
# include "Misc/App.h"
# include "Misc/Optional.h"
2021-05-19 13:37:46 -04:00
# include "Misc/Paths.h"
2021-05-13 21:58:20 -04:00
# include "Internationalization/Regex.h"
DEFINE_LOG_CATEGORY_STATIC ( LogAnalyticsSessionSummary , Verbose , All ) ;
namespace AnalyticsManagerProperties
{
// Those values are added by the manager for internal usage. They are stripped before sending the summary.
static const FString InternalPropertyPrefix = TEXT ( " Internal. " ) ;
static const TAnalyticsProperty < FString > InternalSessionUserId = TEXT ( " Internal.UserId " ) ;
static const TAnalyticsProperty < FString > InternalSessionAppId = TEXT ( " Internal.AppId " ) ;
static const TAnalyticsProperty < FString > InternalSessionAppVersion = TEXT ( " Internal.AppVersion " ) ;
static const TAnalyticsProperty < FString > InternalSessionId = TEXT ( " Internal.SessionId " ) ;
static const TAnalyticsProperty < bool > InternalWasProcessed = TEXT ( " Internal.WasProcessed " ) ;
// Those values are implicitely added by the manager into the report because the analytics backend expecpts them.
2021-05-19 06:22:37 -04:00
static const TAnalyticsProperty < FString > SessionId = TEXT ( " SessionId " ) ;
static const TAnalyticsProperty < FString > ShutdownType = TEXT ( " ShutdownType " ) ;
static const TAnalyticsProperty < bool > DelayedSend = TEXT ( " DelayedSend " ) ;
static const TAnalyticsProperty < FString > SentFrom = TEXT ( " SentFrom " ) ;
static const TAnalyticsProperty < FString > MissingDataFrom = TEXT ( " MissingDataFrom " ) ;
2021-05-13 21:58:20 -04:00
// List of reserved property key.
static TSet < FString > ReservedKeys = {
AnalyticsManagerProperties : : ShutdownType . Key ,
AnalyticsManagerProperties : : DelayedSend . Key ,
2021-05-19 06:22:37 -04:00
AnalyticsManagerProperties : : SentFrom . Key ,
AnalyticsManagerProperties : : MissingDataFrom . Key
2021-05-13 21:58:20 -04:00
} ;
bool IsReserved ( const FString & Key )
{
return ReservedKeys . Contains ( Key ) ;
}
} // namespace AnalyticsManagerProperties
namespace AnalyticsSessionUtils
{
static const FTimespan OrphanSessionCheckPeriod = FTimespan : : FromMinutes ( 5 ) ;
static const FTimespan ProcessGroupDeathGracePeriod = FTimespan : : FromHours ( 1 ) ;
static const FString OrphanSessionsOwnerCriticalSectionName = TEXT ( " UE_AnalyticsSessionSummaryManager_OrphanOwner " ) ;
static const FString OrphanSessionsOwnerFilename = TEXT ( " 8E1D46DBC38F4A789939D781E1B91520 " ) ; // A randomly generated GUID.
FString GetDefaultSavedDir ( )
{
// NOTE: This needs to be a folder common to Epic applications (Editor, CRC, etc.), ideally agnostics from the engine versions to enable picking orphans from
// older versions AND being constant between build configurations (like Editor Developement and CRC Shipping). FPaths functions are sensitive to the
// build configuration and will put files in different places.
return FPaths : : Combine ( FPlatformProcess : : UserSettingsDir ( ) , * FApp : : GetEpicProductIdentifier ( ) , TEXT ( " Common " ) , TEXT ( " Analytics " ) ) ;
}
FRegexPattern GetAnalyticsPropertyFilenamePattern ( )
{
// Filename format is like "ProcessGroupGUID_PID_PID_ProcessName", for example 3E1D7ADBC38F4A789939D781E1B91520_12345_232513_Editor"
return FRegexPattern ( TEXT ( R " ((^[0-9A-Za-z]+) _ ( [ 0 - 9 ] + ) _ ( [ 0 - 9 ] + ) _ ( . * ) ) " )) ; // Need help with regex? Try https://regex101.com/
}
/** Try to acquire the rigth to process and send sessions for orphan groups. */
bool TryAcquiringOrphanGroupsOwnership ( const FString & SessionRootPath , const FString & PrincipalProcessName , uint32 CurrentProcessId )
{
// NOTE: The usage of FPlatformMisc::SetStoredValue()/GetStoredValue() was considered, but those APIs are not thread safe on Mac/Linux
// and concurrent processes can easily stomp the changes made by another one. The implementation below prevent races between threads and processes.
FAnalyticsPropertyStore OwnerStore ;
FString OrphanOwnerFilePathname = SessionRootPath / AnalyticsSessionUtils : : OrphanSessionsOwnerFilename ;
bool bAcquiredOwnership = false ;
// Try to lock. Do not wait if the lock is already taken. That's not a big deal if we delay processing left-over sessions.
FSystemWideCriticalSection SysWideLock ( AnalyticsSessionUtils : : OrphanSessionsOwnerCriticalSectionName , FTimespan : : Zero ( ) ) ;
if ( SysWideLock . IsValid ( ) )
{
if ( IFileManager : : Get ( ) . FileExists ( * OrphanOwnerFilePathname ) )
{
if ( OwnerStore . Load ( OrphanOwnerFilePathname ) )
{
IAnalyticsPropertyStore : : EStatusCode Status = OwnerStore . Set ( PrincipalProcessName , CurrentProcessId ,
[ ] ( const uint32 * Actual , const int32 & ProposedValue ) { return Actual = = nullptr | | * Actual = = 0 | | ! FPlatformProcess : : IsApplicationRunning ( * Actual ) ; } ) ;
bAcquiredOwnership = ( Status = = IAnalyticsPropertyStore : : EStatusCode : : Success ) ;
}
else // If the file exists but fails to load while this process as the system wide lock, that's likely because its content is invalid/corrupted.
{
IFileManager : : Get ( ) . Delete ( * OrphanOwnerFilePathname ) ;
}
}
else if ( OwnerStore . Create ( OrphanOwnerFilePathname , /*Capacity*/ 1 * 1024 ) )
{
bAcquiredOwnership = OwnerStore . Set ( PrincipalProcessName , CurrentProcessId ) = = IAnalyticsPropertyStore : : EStatusCode : : Success ;
}
if ( bAcquiredOwnership )
{
OwnerStore . Flush ( ) ; // Ensure to persist the storage to disk.
}
}
return bAcquiredOwnership ;
}
/** Release the ownership to send sessions for orphan groups. */
void ReleaseOrphanGroupsOwnership ( const FString & SessionRootPath , const FString & PrincipalProcessName , uint32 CurrentProcessId )
{
FString OrphanOwnerFilePathname = SessionRootPath / AnalyticsSessionUtils : : OrphanSessionsOwnerFilename ;
if ( IFileManager : : Get ( ) . FileExists ( * OrphanOwnerFilePathname ) )
{
// Try to lock. Do not wait if the lock is already taken.
FSystemWideCriticalSection SysWideLock ( AnalyticsSessionUtils : : OrphanSessionsOwnerCriticalSectionName , FTimespan : : Zero ( ) ) ;
if ( SysWideLock . IsValid ( ) )
{
// Open the file.
FAnalyticsPropertyStore OwnerStore ;
if ( OwnerStore . Load ( OrphanOwnerFilePathname ) )
{
// Reset to zero for this principal process.
IAnalyticsPropertyStore : : EStatusCode Status = OwnerStore . Set ( PrincipalProcessName , static_cast < uint32 > ( 0 ) ,
[ CurrentProcessId ] ( const uint32 * Actual , const uint32 & Proposed ) { return Actual = = nullptr | | * Actual = = CurrentProcessId ; } ) ;
if ( Status = = IAnalyticsPropertyStore : : EStatusCode : : Success )
{
OwnerStore . Flush ( ) ;
}
}
}
}
}
} // namespace AnalyticsSessionUtils
2021-05-17 20:37:17 -04:00
const TCHAR * LexToString ( EAnalyticsSessionShutdownType ShutdownTypeCode )
{
switch ( ShutdownTypeCode )
{
case EAnalyticsSessionShutdownType : : Shutdown :
return TEXT ( " Shutdown " ) ;
case EAnalyticsSessionShutdownType : : Crashed :
return TEXT ( " Crashed " ) ;
case EAnalyticsSessionShutdownType : : Terminated :
return TEXT ( " Terminated " ) ;
case EAnalyticsSessionShutdownType : : Debugged :
return TEXT ( " Debugger " ) ;
case EAnalyticsSessionShutdownType : : Abnormal :
return TEXT ( " AbnormalShutdown " ) ;
case EAnalyticsSessionShutdownType : : Unknown :
default :
return TEXT ( " Unknown " ) ;
}
}
2021-05-13 21:58:20 -04:00
const TAnalyticsProperty < int32 > FAnalyticsSessionSummaryManager : : ShutdownTypeCodeProperty = TEXT ( " ShutdownTypeCode " ) ;
const TAnalyticsProperty < bool > FAnalyticsSessionSummaryManager : : IsUserLoggingOutProperty = TEXT ( " IsUserLoggingOut " ) ;
FAnalyticsSessionSummaryManager : : FAnalyticsSessionSummaryManager ( const FString & InProcessName , const FString & InProcessGroupId , const FString & InUserId , const FString & InAppId , const FString & InAppVersion , const FString & InSessionId , const FString & InSavedDir )
: FAnalyticsSessionSummaryManager ( InProcessName , InProcessGroupId , FPlatformProcess : : GetCurrentProcessId ( ) , FPlatformProcess : : GetCurrentProcessId ( ) , InUserId , InAppId , InAppVersion , InSessionId , InSavedDir )
{
}
FAnalyticsSessionSummaryManager : : FAnalyticsSessionSummaryManager ( const FString & InProcessName , const FString & InProcessGroupId , uint32 InPrincipalProcessId , const FString & InSavedDir )
: FAnalyticsSessionSummaryManager ( InProcessName , InProcessGroupId , FPlatformProcess : : GetCurrentProcessId ( ) , InPrincipalProcessId , FString ( ) , FString ( ) , FString ( ) , FString ( ) , InSavedDir )
{
}
FAnalyticsSessionSummaryManager : : FAnalyticsSessionSummaryManager ( const FString & InProcessName , const FString & InProcessGroupId , uint32 InCurrentProcessId , uint32 InPrincipalProcessId , const FString & InUserId , const FString & InAppId , const FString & InAppVersion , const FString & InSessionId , const FString & InSavedDir )
: ProcessName ( InProcessName )
, ProcessGroupId ( InProcessGroupId )
, UserId ( InUserId )
, AppId ( InAppId )
, AppVersion ( InAppVersion )
, SessionId ( InSessionId )
, SessionRootPath ( InSavedDir )
, CurrentProcessId ( InCurrentProcessId )
, PrincipalProcessId ( InPrincipalProcessId )
, NextOrphanSessionCheckTimeSecs ( FPlatformTime : : Seconds ( ) )
, bOrphanGroupOwner ( false )
, bIsPrincipal ( InCurrentProcessId = = InPrincipalProcessId )
{
if ( SessionRootPath . IsEmpty ( ) | | ! IFileManager : : Get ( ) . DirectoryExists ( * SessionRootPath ) )
{
SessionRootPath = AnalyticsSessionUtils : : GetDefaultSavedDir ( ) ; // Fallback on the default.
}
// Creates a sub-folder to store the session files.
IFileManager : : Get ( ) . MakeDirectory ( * SessionRootPath , /*Tree*/ true ) ;
// Let principal process deals with its orphans. The principal process is expected to knows its application settings while a subsidiary process may not have any context.
if ( IsPrincipalProcess ( ) )
{
bOrphanGroupOwner = AnalyticsSessionUtils : : TryAcquiringOrphanGroupsOwnership ( SessionRootPath , ProcessName , CurrentProcessId ) ;
}
}
FAnalyticsSessionSummaryManager : : ~ FAnalyticsSessionSummaryManager ( )
{
if ( bOrphanGroupOwner )
{
AnalyticsSessionUtils : : ReleaseOrphanGroupsOwnership ( SessionRootPath , ProcessName , CurrentProcessId ) ;
}
}
TSharedPtr < IAnalyticsPropertyStore > FAnalyticsSessionSummaryManager : : MakeStore ( uint32 InitialCapacity )
{
// NOTE: The ProcessGroupId (a GUID) + PrincipalProcessId is expected to provide a unique key that should not collide with existing files.
FString PropertyStorePathname = SessionRootPath / FString : : Printf ( TEXT ( " %s_%d_%d_%s " ) , * ProcessGroupId , PrincipalProcessId , CurrentProcessId , * ProcessName ) ;
TSharedPtr < FAnalyticsPropertyStore > PropertyStore = MakeShared < FAnalyticsPropertyStore > ( ) ;
if ( PropertyStore & & PropertyStore - > Create ( PropertyStorePathname , InitialCapacity ) )
{
// If this is the principal process for which the session summary is created.
if ( IsPrincipalProcess ( ) )
{
// Add some internal key required to enable a subsidiary process to sent the summary on the behalf of this process.
AnalyticsManagerProperties : : InternalSessionUserId . Set ( PropertyStore . Get ( ) , UserId ) ;
AnalyticsManagerProperties : : InternalSessionAppId . Set ( PropertyStore . Get ( ) , AppId ) ;
AnalyticsManagerProperties : : InternalSessionAppVersion . Set ( PropertyStore . Get ( ) , AppVersion ) ;
AnalyticsManagerProperties : : InternalSessionId . Set ( PropertyStore . Get ( ) , SessionId ) ;
}
}
else
{
PropertyStore . Reset ( ) ; // Ensure to return a null pointer if the store failed to create (not enough disk space for example)
}
return StaticCastSharedPtr < IAnalyticsPropertyStore > ( PropertyStore ) ;
}
void FAnalyticsSessionSummaryManager : : SetSender ( TSharedPtr < IAnalyticsSessionSummarySender > Sender )
{
SummarySender = Sender ;
}
void FAnalyticsSessionSummaryManager : : Shutdown ( bool bDiscard )
{
if ( SummarySender )
{
// Check if this process has something to summarize and send for this instance process group.
TMap < FString , FProcessGroup > PropertyFiles = GetSessionFiles ( ) ;
if ( const FProcessGroup * ProcessGroup = PropertyFiles . Find ( ProcessGroupId ) )
{
if ( bDiscard )
{
CleanupFiles ( ProcessGroup - > PropertyFiles , /*bOnSuccess*/ false ) ;
}
else
{
ProcessSummary ( ProcessGroupId , * ProcessGroup ) ;
}
}
}
else if ( bDiscard )
{
TMap < FString , FProcessGroup > PropertyFiles = GetSessionFiles ( ) ;
if ( const FProcessGroup * ProcessGroup = PropertyFiles . Find ( ProcessGroupId ) )
{
CleanupFiles ( ProcessGroup - > PropertyFiles , /*bOnSuccess*/ false ) ;
}
}
}
void FAnalyticsSessionSummaryManager : : Tick ( )
{
if ( ! SummarySender | | ! IsOrphanGroupsOwner ( ) )
{
return ; // Don't process orphans if the result cannot be sent.
}
if ( FPlatformTime : : Seconds ( ) > NextOrphanSessionCheckTimeSecs )
{
NextOrphanSessionCheckTimeSecs = FPlatformTime : : Seconds ( ) + AnalyticsSessionUtils : : OrphanSessionCheckPeriod . GetTotalSeconds ( ) ;
// Check if this process has something to summarize and send..
TMap < FString , FProcessGroup > PropertyFiles = GetSessionFiles ( ) ;
for ( const TPair < FString , FProcessGroup > & Pair : PropertyFiles )
{
if ( Pair . Value . PrincipalProcessId = = PrincipalProcessId )
{
continue ; // Skip this process session summary. It should not be sent until the last process in the group died.
}
// Picked up an orphan session, decide what to do with it.
ProcessSummary ( /*ProcessGroupId*/ Pair . Key , /*ProcessGroup*/ Pair . Value ) ;
}
}
}
bool FAnalyticsSessionSummaryManager : : IsPrincipalProcess ( ) const
{
return bIsPrincipal ;
}
TMap < FString , FAnalyticsSessionSummaryManager : : FProcessGroup > FAnalyticsSessionSummaryManager : : GetSessionFiles ( ) const
{
TMap < FString , FProcessGroup > ProcessFiles ;
// Visit the files in the session directory.
FRegexPattern FilenamePattern = AnalyticsSessionUtils : : GetAnalyticsPropertyFilenamePattern ( ) ;
IFileManager : : Get ( ) . IterateDirectory ( * SessionRootPath , [ this , & ProcessFiles , & FilenamePattern ] ( const TCHAR * Pathname , bool bIsDirectory ) - > bool
{
if ( ! bIsDirectory )
{
// The property filename is "ProcessGroupGUID_PrincipalProcessId_WriterProcessId_WriterProcessName". The ProcessGroupGUID is expected to have EGuidFormats::Digits -> "00000A00C0000F000000000000000000"
FRegexMatcher Matcher ( FilenamePattern , FPaths : : GetCleanFilename ( Pathname ) ) ;
if ( Matcher . FindNext ( ) )
{
FString FileProcessGroupId = Matcher . GetCaptureGroup ( 1 ) ;
if ( FileProcessGroupId ! = ProcessGroupId & & ! IsOrphanGroupsOwner ( ) )
{
return true ; // This file is from another process group and this manager is not allowed to process it, skip to the next file.
}
FPropertyFileInfo PropertyFileInfo ;
PropertyFileInfo . ProcessId = static_cast < uint32 > ( FCString : : Atoi64 ( * Matcher . GetCaptureGroup ( 3 ) ) ) ;
PropertyFileInfo . ProcessName = Matcher . GetCaptureGroup ( 4 ) ;
PropertyFileInfo . Pathname = Pathname ;
uint32 FilePrincipalProcessId = static_cast < uint32 > ( FCString : : Atoi64 ( * Matcher . GetCaptureGroup ( 2 ) ) ) ;
FProcessGroup & ProcessGroup = ProcessFiles . FindOrAdd ( FileProcessGroupId ) ;
ProcessGroup . PrincipalProcessId = FilePrincipalProcessId ;
if ( FilePrincipalProcessId = = PropertyFileInfo . ProcessId ) // That file was produced by a principal process.
{
ProcessGroup . PrincipalProcessName = PropertyFileInfo . ProcessName ;
}
ProcessGroup . PropertyFiles . Add ( MoveTemp ( PropertyFileInfo ) ) ;
}
}
return true ; // Continue to the next file.
} ) ;
// Can this manager process orphan groups?
if ( IsOrphanGroupsOwner ( ) )
{
check ( IsPrincipalProcess ( ) ) ;
for ( TMap < FString , FProcessGroup > : : TIterator It = ProcessFiles . CreateIterator ( ) ; It ; + + It )
{
// Discard groups that are not from this applications.
if ( ! It . Value ( ) . PrincipalProcessName . IsEmpty ( ) & & It . Value ( ) . PrincipalProcessName ! = ProcessName )
{
It . RemoveCurrent ( ) ;
}
// Discard any other groups that are still running.
else if ( It . Value ( ) . PrincipalProcessId ! = PrincipalProcessId )
{
for ( const FPropertyFileInfo & PropertyFileInfo : It . Value ( ) . PropertyFiles )
{
if ( FPlatformProcess : : IsApplicationRunning ( PropertyFileInfo . ProcessId ) ) // One of the process in that group is still running
{
It . RemoveCurrent ( ) ;
break ;
}
}
}
}
}
return ProcessFiles ;
}
void FAnalyticsSessionSummaryManager : : ProcessSummary ( const FString & InProcessGroupId , const FProcessGroup & InProcessGroup )
{
// Check if this process is the last alive in the group, so that it can process the summary
for ( const FPropertyFileInfo & PropertyFileInfo : InProcessGroup . PropertyFiles )
{
if ( PropertyFileInfo . ProcessId ! = CurrentProcessId )
{
if ( FPlatformProcess : : IsApplicationRunning ( PropertyFileInfo . ProcessId ) )
{
return ; // A process in the group is running, leave this group alone.
}
}
}
// Trying to process a summary for another group than this manager group?
if ( InProcessGroupId ! = ProcessGroupId )
{
checkf ( IsOrphanGroupsOwner ( ) , TEXT ( " Should not process summary of other process group if this wasn't allowed in first place. " ) ) ;
bool bExpired = false ;
// The files discovered tell that processes in the group are dead, but it is unclear if another process in that group is about to create a files or not (because
// of race conditions between processes).
for ( const FPropertyFileInfo & PropertyFileInfo : InProcessGroup . PropertyFiles )
{
// Time since the last file modification.
double FileAgeSecs = IFileManager : : Get ( ) . GetFileAgeSeconds ( * PropertyFileInfo . Pathname ) ;
// Before declaring this group 'dead' leave a reasonable grace period to clear 99% of possible race conditions between processes.
if ( FileAgeSecs > 0 & & FileAgeSecs < = AnalyticsSessionUtils : : ProcessGroupDeathGracePeriod . GetTotalSeconds ( ) )
{
return ; // Wait more before declaring this group dead and processing it.
}
// If the principal file is expired, expire the entire group.
2021-06-01 17:01:37 -04:00
if ( InProcessGroup . PrincipalProcessId = = PropertyFileInfo . ProcessId & & FileAgeSecs > GetSessionExpirationAge ( ) . GetTotalSeconds ( ) )
2021-05-13 21:58:20 -04:00
{
bExpired = true ;
break ;
}
}
// This group is expired or doesn't have the properties from the principal process (incomplete session).
if ( bExpired | | InProcessGroup . PrincipalProcessName . IsEmpty ( ) )
{
// The session data is partial or expired.
CleanupFiles ( InProcessGroup . PropertyFiles , /*bOnSuccess*/ false ) ;
return ;
}
}
// Aggregate the principal and subsidiary process summaries.
2021-05-19 06:22:37 -04:00
TArray < FAnalyticsEventAttribute > InternalProperties ;
TArray < FAnalyticsEventAttribute > SummaryProperties ;
if ( ! AggregateSummaries ( InProcessGroupId , InProcessGroup , SummaryProperties , InternalProperties ) )
2021-05-13 21:58:20 -04:00
{
CleanupFiles ( InProcessGroup . PropertyFiles , /*bOnSuccess*/ false ) ;
return ;
}
// Was the group aggregated above the group of this principal process?
if ( IsPrincipalProcess ( ) & & InProcessGroupId = = ProcessGroupId )
{
// Clean up before sending, so that we don't send twice. (In case of crash while sending, we will not know if the summary was sent or not).
CleanupFiles ( InProcessGroup . PropertyFiles , /*bOnSuccess*/ true ) ;
SummarySender - > SendSessionSummary ( UserId , AppId , AppVersion , SessionId , SummaryProperties ) ;
}
// Send the session summary on behalf of the principal process that created it.
else
{
// The principal process saves those internal properties to let a subsidiary or another principal processes submit the summary on its behalf.
2021-05-19 06:22:37 -04:00
const FAnalyticsEventAttribute * UserIdProp = InternalProperties . FindByPredicate ( [ ] ( const FAnalyticsEventAttribute & Candidate ) { return Candidate . GetName ( ) = = AnalyticsManagerProperties : : InternalSessionUserId . Key ; } ) ;
const FAnalyticsEventAttribute * AppIdProp = InternalProperties . FindByPredicate ( [ ] ( const FAnalyticsEventAttribute & Candidate ) { return Candidate . GetName ( ) = = AnalyticsManagerProperties : : InternalSessionAppId . Key ; } ) ;
const FAnalyticsEventAttribute * AppVersionProp = InternalProperties . FindByPredicate ( [ ] ( const FAnalyticsEventAttribute & Candidate ) { return Candidate . GetName ( ) = = AnalyticsManagerProperties : : InternalSessionAppVersion . Key ; } ) ;
const FAnalyticsEventAttribute * SessionIdProp = InternalProperties . FindByPredicate ( [ ] ( const FAnalyticsEventAttribute & Candidate ) { return Candidate . GetName ( ) = = AnalyticsManagerProperties : : InternalSessionId . Key ; } ) ;
2021-05-13 21:58:20 -04:00
if ( UserIdProp & & AppIdProp & & AppVersionProp & & SessionIdProp )
{
CleanupFiles ( InProcessGroup . PropertyFiles , /*bOnSuccess*/ true ) ;
2021-05-19 06:22:37 -04:00
SummarySender - > SendSessionSummary ( UserIdProp - > GetValue ( ) , AppIdProp - > GetValue ( ) , AppVersionProp - > GetValue ( ) , SessionIdProp - > GetValue ( ) , SummaryProperties ) ;
2021-05-13 21:58:20 -04:00
}
else
{
CleanupFiles ( InProcessGroup . PropertyFiles , /*bOnSuccess*/ false ) ;
}
}
}
2021-05-19 06:22:37 -04:00
bool FAnalyticsSessionSummaryManager : : AggregateSummaries ( const FString & InProcessGroupId , const FProcessGroup & ProcessGroup , TArray < FAnalyticsEventAttribute > & OutSummaryProperties , TArray < FAnalyticsEventAttribute > & OutInternalProperties )
2021-05-13 21:58:20 -04:00
{
FAnalyticsPropertyStore PropertyStore ;
2021-05-19 06:22:37 -04:00
TSet < FString > SummaryKeys ;
auto RenameKey = [ ] ( const FString & KeyToRename , const FString & FromProcess , const TSet < FString > & ExistingKeys , const TCHAR * Reason )
2021-05-13 21:58:20 -04:00
{
FString RenamedKey ;
int32 Index = 1 ;
do
{
RenamedKey = FString : : Printf ( TEXT ( " %s_%d " ) , * KeyToRename , Index + + ) ;
} while ( ExistingKeys . Contains ( RenamedKey ) ) ;
UE_LOG ( LogAnalyticsSessionSummary , Warning , TEXT ( " Renamed key '%s' from process '%s' as '%s'. Reason: %s " ) , * KeyToRename , * FromProcess , * RenamedKey , Reason ) ;
return RenamedKey ;
} ;
2021-05-19 06:22:37 -04:00
TArray < FString > MissingDataFromProcesses ;
2021-05-13 21:58:20 -04:00
// Merge the properties collected by the principal and its subsidiary processes.
2021-05-19 06:22:37 -04:00
for ( const FPropertyFileInfo & PropertyFileInfo : ProcessGroup . PropertyFiles )
2021-05-13 21:58:20 -04:00
{
if ( PropertyStore . Load ( PropertyFileInfo . Pathname ) )
{
if ( PropertyStore . Contains ( AnalyticsManagerProperties : : InternalWasProcessed . Key ) )
{
return false ; // We don't know if the manager failed to delete before of after the session was sent. Ignore this group.
}
int32 ShutdownTypeCode ;
if ( PropertyStore . Get ( ShutdownTypeCodeProperty . Key , ShutdownTypeCode ) = = IAnalyticsPropertyStore : : EStatusCode : : Success )
{
// Don't add 'ShutdownType' more than once. If 'ShutdownTypeCode' key used to derive the 'ShutdownType' is duplicated, the code will emit a warning and rename the second instance.
2021-05-19 06:22:37 -04:00
if ( ! SummaryKeys . Contains ( AnalyticsManagerProperties : : ShutdownType . Key ) )
2021-05-13 21:58:20 -04:00
{
// Convert 'ShutdownTypeCode' into its string representation and emit it as 'ShutdownType' as known by the analytics backend.
2021-05-17 20:37:17 -04:00
OutSummaryProperties . Emplace ( AnalyticsManagerProperties : : ShutdownType . Key , LexToString ( static_cast < EAnalyticsSessionShutdownType > ( ShutdownTypeCode ) ) ) ;
2021-05-19 06:22:37 -04:00
SummaryKeys . Add ( AnalyticsManagerProperties : : ShutdownType . Key ) ;
2021-05-13 21:58:20 -04:00
}
}
2021-05-19 06:22:37 -04:00
PropertyStore . VisitAll ( [ & PropertyFileInfo , & OutSummaryProperties , & OutInternalProperties , & RenameKey , & SummaryKeys ] ( FAnalyticsEventAttribute & & Attr )
2021-05-13 21:58:20 -04:00
{
2021-05-19 06:22:37 -04:00
if ( Attr . GetName ( ) . StartsWith ( AnalyticsManagerProperties : : InternalPropertyPrefix ) )
2021-05-13 21:58:20 -04:00
{
2021-05-19 06:22:37 -04:00
SummaryKeys . Add ( Attr . GetName ( ) ) ;
OutInternalProperties . Emplace ( Attr ) ;
2021-05-13 21:58:20 -04:00
}
2021-05-19 06:22:37 -04:00
else if ( AnalyticsManagerProperties : : IsReserved ( Attr . GetName ( ) ) )
2021-05-13 21:58:20 -04:00
{
2021-05-19 06:22:37 -04:00
FString RenamedKey = RenameKey ( Attr . GetName ( ) , PropertyFileInfo . ProcessName , SummaryKeys , TEXT ( " Key name is reserved. " ) ) ;
SummaryKeys . Add ( RenamedKey ) ;
OutSummaryProperties . Emplace ( RenamedKey , Attr . GetValue ( ) ) ;
2021-05-13 21:58:20 -04:00
}
2021-05-19 06:22:37 -04:00
else if ( ! SummaryKeys . Contains ( Attr . GetName ( ) ) )
2021-05-13 21:58:20 -04:00
{
2021-05-19 06:22:37 -04:00
SummaryKeys . Add ( Attr . GetName ( ) ) ;
OutSummaryProperties . Emplace ( Attr ) ;
2021-05-13 21:58:20 -04:00
}
else // Deal with key collisions. Keys are supposed to be unique. Duplication should be easy to spot during developement.
{
2021-05-19 06:22:37 -04:00
FString RenamedKey = RenameKey ( Attr . GetName ( ) , PropertyFileInfo . ProcessName , SummaryKeys , TEXT ( " Key collision between processes of a group. " ) ) ;
SummaryKeys . Add ( Attr . GetName ( ) ) ;
OutSummaryProperties . Emplace ( RenamedKey , Attr . GetValue ( ) ) ;
2021-05-13 21:58:20 -04:00
}
} ) ;
// Mark this file as processed to avoid sending the same session twice.
AnalyticsManagerProperties : : InternalWasProcessed . Set ( & PropertyStore , true ) ;
PropertyStore . Flush ( ) ;
}
2021-05-19 06:22:37 -04:00
else if ( PropertyFileInfo . ProcessId = = ProcessGroup . PrincipalProcessId )
{
// Without the principal process data, too much data is missing, abort.
return false ;
}
2021-05-13 21:58:20 -04:00
else
{
2021-05-19 06:22:37 -04:00
// Subsidiary process data will be missing. Not ideal, but this should only be complementary info for the principal process, flag it and continue.
MissingDataFromProcesses . Add ( PropertyFileInfo . ProcessName ) ;
2021-05-13 21:58:20 -04:00
}
}
// The manager saves this key in the store it creates.
2021-05-19 06:22:37 -04:00
if ( const FAnalyticsEventAttribute * InternalSessionId = OutInternalProperties . FindByPredicate ( [ ] ( const FAnalyticsEventAttribute & Candidate ) { return Candidate . GetName ( ) = = AnalyticsManagerProperties : : InternalSessionId . Key ; } ) )
2021-05-13 21:58:20 -04:00
{
2021-05-19 06:22:37 -04:00
OutSummaryProperties . Emplace ( AnalyticsManagerProperties : : SessionId . Key , InternalSessionId - > GetValue ( ) ) ;
}
2021-05-27 13:40:37 -04:00
if ( MissingDataFromProcesses . Num ( ) )
2021-05-19 06:22:37 -04:00
{
// Add the subsidiary process tag name(s) for which the data file couldn't be loaded (file was locked, corrupted, ...)
OutSummaryProperties . Emplace ( AnalyticsManagerProperties : : MissingDataFrom . Key , FString : : Join ( MissingDataFromProcesses , TEXT ( " , " ) ) ) ;
2021-05-13 21:58:20 -04:00
}
// The summary is sent delayed when it is processed from another group.
2021-05-19 06:22:37 -04:00
OutSummaryProperties . Emplace ( AnalyticsManagerProperties : : DelayedSend . Key , InProcessGroupId ! = ProcessGroupId ) ;
2021-05-13 21:58:20 -04:00
// This process is about to send the report, record the process name in the summary.
OutSummaryProperties . Emplace ( AnalyticsManagerProperties : : SentFrom . Key , ProcessName ) ;
return true ;
}
bool FAnalyticsSessionSummaryManager : : CleanupFiles ( const TArray < FPropertyFileInfo > & PropertyFiles , bool bOnSuccess )
{
// TODO as future work: Compute how many times we clean up on failure. Keep track of it and periodically send an analytic event about it.
bool bAllDeleted = true ;
for ( const FPropertyFileInfo & PropertyFileInfo : PropertyFiles )
{
bAllDeleted & = IFileManager : : Get ( ) . Delete ( * PropertyFileInfo . Pathname , /*MustExist*/ false , /*EvenReadOnly*/ true , /*Quiet*/ true ) ;
}
return bAllDeleted ;
}
void FAnalyticsSessionSummaryManager : : CleanupExpiredFiles ( const FString & SavedDir )
{
FString SessionRootPath = SavedDir . IsEmpty ( ) ? AnalyticsSessionUtils : : GetDefaultSavedDir ( ) : SavedDir ;
// The manager stores its files under its own folder.
if ( ! IFileManager : : Get ( ) . DirectoryExists ( * SessionRootPath ) )
{
return ;
}
// Visit the files in the session directory.
FRegexPattern FilenamePattern = AnalyticsSessionUtils : : GetAnalyticsPropertyFilenamePattern ( ) ;
IFileManager : : Get ( ) . IterateDirectory ( * SessionRootPath , [ & FilenamePattern ] ( const TCHAR * Pathname , bool bIsDirectory ) - > bool
{
if ( bIsDirectory )
{
return true ; // Continue.
}
FRegexMatcher Matcher ( FilenamePattern , FPaths : : GetCleanFilename ( Pathname ) ) ;
if ( Matcher . FindNext ( ) )
{
// Time since the last file modification.
double FileAgeSecs = IFileManager : : Get ( ) . GetFileAgeSeconds ( Pathname ) ;
2021-06-01 17:01:37 -04:00
// Add up an extra day to the normal expiration delay so that we never clean up files that are on the edge.
if ( FileAgeSecs > GetSessionExpirationAge ( ) . GetTotalSeconds ( ) + FTimespan : : FromDays ( 1.0 ) . GetTotalSeconds ( ) )
2021-05-13 21:58:20 -04:00
{
IFileManager : : Get ( ) . Delete ( Pathname ) ;
}
}
return true ;
} ) ;
}