Files
FlaxEngine/Source/Editor/Analytics/EditorAnalytics.cpp
2020-12-23 21:54:21 +01:00

273 lines
7.7 KiB
C++

// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "EditorAnalytics.h"
#include "EditorAnalyticsController.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Vector2.h"
#include "Engine/Core/Types/DateTime.h"
#include "Editor/Editor.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Utilities/StringConverter.h"
#include "FlaxEngine.Gen.h"
#include <ThirdParty/UniversalAnalytics/universal-analytics.h>
#define FLAX_EDITOR_GOOGLE_ID "UA-88357703-3"
// Helper doc: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters
namespace EditorAnalyticsImpl
{
UATracker Tracker = nullptr;
StringAnsi ClientId;
StringAnsi ProjectName;
StringAnsi ScreenResolution;
StringAnsi UserLanguage;
StringAnsi GPU;
DateTime SessionStartTime;
CriticalSection Locker;
bool IsSessionActive = false;
EditorAnalyticsController Controller;
Array<char> TmpBuffer;
}
using namespace EditorAnalyticsImpl;
class EditorAnalyticsService : public EngineService
{
public:
EditorAnalyticsService()
: EngineService(TEXT("Editor Analytics"))
{
}
bool Init() override;
void Dispose() override;
};
EditorAnalyticsService EditorAnalyticsServiceInstance;
bool EditorAnalytics::IsSessionActive()
{
return EditorAnalyticsImpl::IsSessionActive;
}
void EditorAnalytics::StartSession()
{
ScopeLock lock(Locker);
if (EditorAnalyticsImpl::IsSessionActive)
return;
// Prepare client metadata
if (ClientId.IsEmpty())
{
ClientId = Platform::GetUniqueDeviceId().ToString(Guid::FormatType::N).ToStringAnsi();
}
if (ScreenResolution.IsEmpty())
{
const Vector2 desktopSize = Platform::GetDesktopSize();
ScreenResolution = StringAnsi(StringUtils::ToString((int32)desktopSize.X)) + "x" + StringAnsi(StringUtils::ToString((int32)desktopSize.Y));
}
if (UserLanguage.IsEmpty())
{
UserLanguage = Platform::GetUserLocaleName().ToStringAnsi();
}
if (GPU.IsEmpty())
{
const auto gpu = GPUDevice::Instance;
if (gpu && gpu->GetState() == GPUDevice::DeviceState::Ready)
GPU = StringAsANSI<>(gpu->GetAdapter()->GetDescription().GetText()).Get();
}
if (ProjectName.IsEmpty())
{
ProjectName = Editor::Project->Name.ToStringAnsi();
}
SessionStartTime = DateTime::Now();
// Initialize the analytics tracker
Tracker = createTracker(FLAX_EDITOR_GOOGLE_ID, ClientId.Get(), nullptr);
Tracker->user_agent = "Flax Editor";
// Store these options permanently (for the lifetime of the tracker)
setTrackerOption(Tracker, UA_OPTION_QUEUE, 1);
UASettings GlobalSettings =
{
{
{ UA_DOCUMENT_PATH, 0, "Flax Editor" },
{ UA_DOCUMENT_TITLE, 0, "Flax Editor" },
#if PLATFORM_WINDOWS
{ UA_USER_AGENT, 0, "Windows " FLAXENGINE_VERSION_TEXT },
#elif PLATFORM_LINUX
{ UA_USER_AGENT, 0, "Linux " FLAXENGINE_VERSION_TEXT },
#else
#error "Unknown platform"
#endif
{ UA_ANONYMIZE_IP, 0, "0" },
{ UA_APP_ID, 0, "Flax Editor " FLAXENGINE_VERSION_TEXT },
{ UA_APP_INSTALLER_ID, 0, "Flax Editor" },
{ UA_APP_NAME, 0, "Flax Editor" },
{ UA_APP_VERSION, 0, FLAXENGINE_VERSION_TEXT },
{ UA_SCREEN_NAME, 0, "Flax Editor " FLAXENGINE_VERSION_TEXT },
{ UA_SCREEN_RESOLUTION, 0, ScreenResolution.Get() },
{ UA_USER_LANGUAGE, 0, UserLanguage.Get() },
}
};
setParameters(Tracker, &GlobalSettings);
// Send the initial session event
UAOptions sessionViewOptions =
{
{
{ UA_EVENT_CATEGORY, 0, "Session" },
{ UA_EVENT_ACTION, 0, "Start Editor" },
{ UA_EVENT_LABEL, 0, "Start Editor" },
{ UA_SESSION_CONTROL, 0, "start" },
{ UA_DOCUMENT_TITLE, 0, ProjectName.Get() },
}
};
sendTracking(Tracker, UA_SCREENVIEW, &sessionViewOptions);
EditorAnalyticsImpl::IsSessionActive = true;
Controller.Init();
// Report GPU model
if (GPU.HasChars())
{
SendEvent("Telemetry", "GPU.Model", GPU.Get());
}
}
void EditorAnalytics::EndSession()
{
ScopeLock lock(Locker);
if (!EditorAnalyticsImpl::IsSessionActive)
return;
Controller.Cleanup();
StringAnsi sessionLength = StringAnsi::Format("{0}", (int32)(DateTime::Now() - SessionStartTime).GetTotalSeconds());
// Send the end session event
UAOptions sessionEventOptions =
{
{
{ UA_EVENT_CATEGORY, 0, "Session" },
{ UA_EVENT_ACTION, 0, "Session Length" },
{ UA_EVENT_LABEL, 0, "Session Length" },
{ UA_EVENT_VALUE, 0, sessionLength.Get() },
{ UA_CUSTOM_DIMENSION, 1, "Session Length" },
{ UA_CUSTOM_METRIC, 1, sessionLength.Get() },
}
};
sendTracking(Tracker, UA_EVENT, &sessionEventOptions);
// Send the end session event
UAOptions sessionViewOptions =
{
{
{ UA_EVENT_CATEGORY, 0, "Session" },
{ UA_EVENT_ACTION, 0, "End Editor" },
{ UA_EVENT_LABEL, 0, "End Editor" },
{ UA_EVENT_VALUE, 0, sessionLength.Get() },
{ UA_SESSION_CONTROL, 0, "end" },
}
};
sendTracking(Tracker, UA_SCREENVIEW, &sessionViewOptions);
// Cleanup
removeTracker(Tracker);
Tracker = nullptr;
EditorAnalyticsImpl::IsSessionActive = false;
}
void EditorAnalytics::SendEvent(const char* category, const char* name, const char* label)
{
ScopeLock lock(Locker);
if (!EditorAnalyticsImpl::IsSessionActive)
return;
UAOptions opts =
{
{
{ UA_EVENT_CATEGORY, 0, (char*)category },
{ UA_EVENT_ACTION, 0, (char*)name },
{ UA_EVENT_LABEL, 0, (char*)label },
}
};
sendTracking(Tracker, UA_EVENT, &opts);
}
void EditorAnalytics::SendEvent(const char* category, const char* name, const StringView& label)
{
SendEvent(category, name, label.Get());
}
void EditorAnalytics::SendEvent(const char* category, const char* name, const Char* label)
{
ScopeLock lock(Locker);
if (!EditorAnalyticsImpl::IsSessionActive)
return;
ASSERT(category && name && label);
const int32 labelLength = StringUtils::Length(label);
TmpBuffer.Clear();
TmpBuffer.Resize(labelLength + 1);
StringUtils::ConvertUTF162ANSI(label, TmpBuffer.Get(), labelLength);
TmpBuffer[labelLength] = 0;
UAOptions opts =
{
{
{ UA_EVENT_CATEGORY, 0, (char*)category },
{ UA_EVENT_ACTION, 0, (char*)name },
{ UA_EVENT_LABEL, 0, (char*)TmpBuffer.Get() },
}
};
sendTracking(Tracker, UA_EVENT, &opts);
}
bool EditorAnalyticsService::Init()
{
#if COMPILE_WITH_DEV_ENV
// Disable analytics from the dev (internal) builds
LOG(Info, "Editor analytics service is disabled in dev builds.");
return false;
#endif
// If file '<editor_install_root>/noTracking' exists then do not use analytics (per engine instance)
// If file '%appdata/Flax/noTracking' exists then do not use analytics (globally)
String appDataPath;
FileSystem::GetSpecialFolderPath(SpecialFolder::AppData, appDataPath);
if (FileSystem::FileExists(Globals::StartupFolder / TEXT("noTracking")) ||
FileSystem::FileExists(appDataPath / TEXT("Flax/noTracking")))
{
LOG(Info, "Editor analytics service is disabled.");
return false;
}
LOG(Info, "Editor analytics service is enabled. Curl version: {0}", TEXT(LIBCURL_VERSION));
EditorAnalytics::StartSession();
return false;
}
void EditorAnalyticsService::Dispose()
{
EditorAnalytics::EndSession();
}