mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-05 05:07:48 +00:00
537 lines
15 KiB
C++
537 lines
15 KiB
C++
#include "stdafx.h"
|
|
|
|
#include "Util/Analytics.h"
|
|
#include "util/ThreadPool.h"
|
|
#include "Util/Http.h"
|
|
#include "Util/Statistics.h"
|
|
#include "Util/RobloxGoogleAnalytics.h"
|
|
|
|
#include "v8datamodel/GameBasicSettings.h"
|
|
#include "v8datamodel/DebugSettings.h"
|
|
|
|
#include "RobloxServicesTools.h"
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
using namespace RBX;
|
|
|
|
DYNAMIC_LOGVARIABLE(AnalyticsLog, 0)
|
|
|
|
// google analytics
|
|
DYNAMIC_LOGGROUP(GoogleAnalyticsTracking)
|
|
FASTFLAG(GoogleAnalyticsTrackingEnabled)
|
|
FASTFLAG(DebugAnalyticsForceLotteryWin)
|
|
DYNAMIC_FASTSTRING(RobloxAnalyticsURL)
|
|
DYNAMIC_FASTFLAG(RobloxAnalyticsTrackingEnabled)
|
|
DYNAMIC_FASTFLAG(DebugAnalyticsSendUserId)
|
|
FASTFLAG(SendStudioEventsWithStudioSID)
|
|
FASTFLAG(UseBuildGenericGameUrl)
|
|
|
|
// influx
|
|
DYNAMIC_FASTSTRING(HttpInfluxURL)
|
|
DYNAMIC_FASTSTRING(HttpInfluxDatabase)
|
|
DYNAMIC_FASTSTRING(HttpInfluxUser)
|
|
DYNAMIC_FASTSTRING(HttpInfluxPassword)
|
|
DYNAMIC_FASTFLAGVARIABLE(InfluxDb09Enabled, false)
|
|
|
|
namespace { // http utils
|
|
|
|
const int kNumberThreadPoolThreads = 16;
|
|
ThreadPool defaultThreadPool(kNumberThreadPoolThreads, BaseThreadPool::NoAction, 500);
|
|
|
|
void httpHandler(std::string url, std::string params, std::string optionalContentType, bool isGet, bool externalRequest)
|
|
{
|
|
std::istringstream sparams(params);
|
|
std::string response;
|
|
|
|
std::string fullUrl = isGet ? format("%s?%s", url.c_str(), params.c_str()) : url;
|
|
|
|
RBX::Http http(fullUrl);
|
|
http.recordStatistics = false;
|
|
http.doNotUseCachedResponse = true;
|
|
try
|
|
{
|
|
if(isGet)
|
|
http.get(response);
|
|
else
|
|
http.post(sparams, optionalContentType.empty() ? Http::kContentTypeUrlEncoded : optionalContentType, params.size() > 256, response, externalRequest);
|
|
}
|
|
catch (const RBX::base_exception &ex)
|
|
{
|
|
FASTLOGS(DFLog::AnalyticsLog, "Exception in analytics httpHandler: %s", ex.what());
|
|
}
|
|
}
|
|
|
|
void postOrGet(ThreadPool& threadPool, const std::string& url, const std::string& data, const std::string& optionalContentType, bool blocking, bool isGet, bool externalRequest)
|
|
{
|
|
if (blocking)
|
|
{
|
|
httpHandler(url, data, optionalContentType, isGet, externalRequest);
|
|
}
|
|
else
|
|
{
|
|
bool didSchedule = threadPool.schedule(boost::bind(&httpHandler, url, data, optionalContentType, isGet, externalRequest));
|
|
RBXASSERT(didSchedule);
|
|
if (!didSchedule)
|
|
{
|
|
FASTLOGS(DFLog::AnalyticsLog, "Analytics task was not scheduled in the threadpool: %s", data.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace RBX {
|
|
namespace Analytics {
|
|
|
|
static std::string appVersion;
|
|
static std::string reporter;
|
|
static std::string location;
|
|
static std::string userId;
|
|
static std::string placeId;
|
|
|
|
std::string sanitizeParam(const std::string& p)
|
|
{
|
|
std::string result = p;
|
|
result.erase(std::remove_if(result.begin(), result.end(), boost::is_any_of(", ")), result.end());
|
|
return result;
|
|
}
|
|
|
|
void setUserId(int id)
|
|
{
|
|
userId = RBX::format("%d", id);
|
|
}
|
|
|
|
void setPlaceId(int id)
|
|
{
|
|
placeId = RBX::format("%d", id);
|
|
}
|
|
|
|
void setAppVersion(const std::string& version)
|
|
{
|
|
appVersion = sanitizeParam(version);
|
|
}
|
|
|
|
void setLocation(const std::string& loc)
|
|
{
|
|
location = sanitizeParam(loc);
|
|
}
|
|
|
|
void setReporter(const std::string& rep)
|
|
{
|
|
reporter = sanitizeParam(rep);
|
|
}
|
|
|
|
namespace EphemeralCounter
|
|
{
|
|
const std::string countersApiKey = "76E5A40C-3AE1-4028-9F10-7C62520BD94F";
|
|
ThreadPool countersThreadPool(kNumberThreadPoolThreads, BaseThreadPool::NoAction, 500);
|
|
|
|
void reportStats(const std::string& category, float value, bool blocking)
|
|
{
|
|
std::string baseUrl = ::GetBaseURL();
|
|
if (baseUrl[baseUrl.size() - 1] != '/')
|
|
baseUrl += '/';
|
|
|
|
std::string valueStr = RBX::format("%f", value);
|
|
std::string url;
|
|
if (FFlag::UseBuildGenericGameUrl)
|
|
{
|
|
url = BuildGenericGameUrl(baseUrl, RBX::format("game/report-stats?name=%s&value=%s", Http::urlEncode(category).c_str(), Http::urlEncode(valueStr).c_str()));
|
|
}
|
|
else
|
|
{
|
|
url = RBX::format("%sgame/report-stats?name=%s&value=%s", baseUrl.c_str(), Http::urlEncode(category).c_str(), Http::urlEncode(valueStr).c_str());
|
|
}
|
|
|
|
// post
|
|
postOrGet(countersThreadPool, url, "", Http::kContentTypeUrlEncoded, blocking, false, false);
|
|
}
|
|
|
|
void reportCountersCSV(const std::string& counterNamesCSV, bool blocking)
|
|
{
|
|
std::string counterUrl = GetCountersMultiIncrementUrl(::GetBaseURL(), countersApiKey);
|
|
std::string data = "counterNamesCsv=" + counterNamesCSV;
|
|
|
|
// post
|
|
postOrGet(countersThreadPool, counterUrl, data, Http::kContentTypeUrlEncoded, blocking, false, false);
|
|
}
|
|
|
|
void reportCounter(const std::string& counterName, int amount, bool blocking)
|
|
{
|
|
std::string counterUrl = GetCountersUrl(::GetBaseURL(), countersApiKey);
|
|
std::string url = RBX::format("%s&counterName=%s&amount=%d", counterUrl.c_str(), counterName.c_str(), amount);
|
|
|
|
// post
|
|
postOrGet(countersThreadPool, url, "", Http::kContentTypeUrlEncoded, blocking, false, false);
|
|
}
|
|
|
|
} // namespace EphemeralCounter
|
|
|
|
namespace GoogleAnalytics {
|
|
|
|
bool initialized = false;
|
|
bool canUseGA = false;
|
|
bool canUserRobloxEvents = false;
|
|
std::string googleClientID;
|
|
std::string googleAccountPropertyID;
|
|
std::string robloxProductName;
|
|
std::string experimentString;
|
|
std::string robloxSessionKey;
|
|
static rbx::signal<void()> analyticTrackedSignalVar;
|
|
|
|
bool atteptedToUseBeforeInit = false;
|
|
|
|
std::string generateClientID()
|
|
{
|
|
RBX::Guid guid;
|
|
std::string clientGUID;
|
|
guid.generateStandardGUID(clientGUID);
|
|
|
|
// OS Generated GUIDs come in braced form ie "{f81d4fae-7dec-11d0-a765-00a0c91e6bf6}"
|
|
// whereas RFC 4122 http://www.ietf.org/rfc/rfc4122.txt is non-braced.
|
|
clientGUID.erase(std::find(clientGUID.begin(), clientGUID.end(), '{'));
|
|
clientGUID.erase(std::find(clientGUID.begin(), clientGUID.end(), '}'));
|
|
|
|
return clientGUID;
|
|
}
|
|
|
|
inline std::string googleCollectionParams(const std::string &hitType)
|
|
{
|
|
std::stringstream ss;
|
|
ss
|
|
<< "v=1"
|
|
<< "&tid=" << googleAccountPropertyID
|
|
<< "&cid=" << googleClientID
|
|
<< "&t=" << hitType;
|
|
|
|
if (DFFlag::DebugAnalyticsSendUserId && userId.length() > 0)
|
|
ss << "&userId=" << userId;
|
|
|
|
return ss.str();
|
|
}
|
|
|
|
void lotteryInit(const std::string &accountPropertyID, int lotteryThreshold, const std::string& productName /*= ""*/, int robloxAnalyticsLottery /* = -1 */, const std::string &sessionKey /* = "sessionID=" */)
|
|
{
|
|
GoogleAnalytics::init(accountPropertyID, productName);
|
|
|
|
robloxSessionKey = sessionKey;
|
|
|
|
if(robloxAnalyticsLottery == -1)
|
|
{
|
|
robloxAnalyticsLottery = lotteryThreshold;
|
|
}
|
|
|
|
std::size_t lottery = boost::hash_value(googleClientID) % 100;
|
|
FASTLOG1(DFLog::GoogleAnalyticsTracking, "Google analytics lottery number = %d", lottery);
|
|
// initialize google analytics
|
|
if (lottery < (std::size_t)lotteryThreshold || FFlag::DebugAnalyticsForceLotteryWin)
|
|
{
|
|
canUseGA = true;
|
|
}
|
|
if (lottery < (std::size_t)robloxAnalyticsLottery || FFlag::DebugAnalyticsForceLotteryWin)
|
|
{
|
|
canUserRobloxEvents = true;
|
|
}
|
|
}
|
|
|
|
void init(const std::string &accountPropertyID, const std::string& productName)
|
|
{
|
|
if(atteptedToUseBeforeInit)
|
|
RBXASSERT(false); // Did you try to send events before analytics is initialized? Set breakpoints to trackEvent* to find out.
|
|
|
|
if (initialized)
|
|
{
|
|
FASTLOGS(DFLog::GoogleAnalyticsTracking, "%s", "Google analytics already initialized!");
|
|
return;
|
|
}
|
|
|
|
robloxProductName = productName;
|
|
|
|
googleAccountPropertyID = accountPropertyID;
|
|
RBXASSERT(googleAccountPropertyID.length() > 0);
|
|
|
|
std::string clientId = GameBasicSettings::singleton().getGoogleAnalyticsClientId();
|
|
if (clientId.length() == 0)
|
|
{
|
|
// generate and store new client ID
|
|
clientId = generateClientID();
|
|
GameBasicSettings::singleton().setGoogleAnalyticsClientId(clientId);
|
|
}
|
|
|
|
googleClientID = clientId;
|
|
RBXASSERT(googleClientID.length() > 0);
|
|
|
|
initialized = true;
|
|
}
|
|
|
|
bool getCanUse()
|
|
{
|
|
return canUseGA || canUserRobloxEvents;
|
|
}
|
|
|
|
void setCanUse()
|
|
{
|
|
canUseGA = true;
|
|
canUserRobloxEvents = true;
|
|
}
|
|
|
|
void sendEventGA(const char* category,
|
|
const char* action,
|
|
const char* label,
|
|
int value,
|
|
bool sync)
|
|
{
|
|
std::stringstream params;
|
|
params
|
|
<< googleCollectionParams("event")
|
|
<< "&ec=" << category
|
|
<< "&ea=" << action
|
|
<< "&ev=" << value
|
|
<< "&el=" << label;
|
|
|
|
postOrGet(defaultThreadPool, RobloxGoogleAnalytics::kGoogleAnalyticsBaseURL, params.str(), Http::kContentTypeDefaultUnspecified, sync, false, true);
|
|
}
|
|
|
|
void sendEventRoblox(const char* category,
|
|
const char* action,
|
|
const char* label,
|
|
int value,
|
|
bool sync)
|
|
{
|
|
if (!initialized)
|
|
{
|
|
atteptedToUseBeforeInit = true;
|
|
return;
|
|
}
|
|
|
|
if(DFFlag::RobloxAnalyticsTrackingEnabled && !robloxProductName.empty())
|
|
{
|
|
std::stringstream params;
|
|
params
|
|
<< (FFlag::SendStudioEventsWithStudioSID ? robloxSessionKey : "sessionId=") << googleClientID
|
|
<< "&userID=" << userId
|
|
<< "&placeID=" << placeId
|
|
<< "&category=" << category
|
|
<< "&evt=" << action
|
|
<< "&label=" << label
|
|
<< "&value=" << value
|
|
<< experimentString;
|
|
|
|
postOrGet(defaultThreadPool, RBX::format("%s%s/e.png",DFString::RobloxAnalyticsURL.c_str(), robloxProductName.c_str()), params.str(), Http::kContentTypeDefaultUnspecified, sync, true, true);
|
|
}
|
|
}
|
|
|
|
void trackEvent(const char *category,
|
|
const char *action,
|
|
const char *label,
|
|
int value,
|
|
bool sync)
|
|
{
|
|
if (!initialized)
|
|
{
|
|
atteptedToUseBeforeInit = true;
|
|
return;
|
|
}
|
|
|
|
if(canUseGA)
|
|
{
|
|
sendEventGA(category, action, label, value, sync);
|
|
}
|
|
|
|
if(canUserRobloxEvents)
|
|
{
|
|
sendEventRoblox(category, action, label, value, sync);
|
|
}
|
|
|
|
analyticTrackedSignalVar();
|
|
}
|
|
|
|
void trackEventWithoutThrottling(const char* category,
|
|
const char* action,
|
|
const char* label,
|
|
int value,
|
|
bool sync)
|
|
{
|
|
if (!initialized)
|
|
{
|
|
atteptedToUseBeforeInit = true;
|
|
return;
|
|
}
|
|
|
|
sendEventGA(category, action, label, value, sync);
|
|
sendEventRoblox(category, action, label, value, sync);
|
|
|
|
analyticTrackedSignalVar();
|
|
}
|
|
|
|
void trackUserTiming(const char *category,
|
|
const char *variable,
|
|
int milliseconds,
|
|
const char *label,
|
|
bool sync)
|
|
{
|
|
if (!initialized || !canUseGA)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::stringstream params;
|
|
params
|
|
<< googleCollectionParams("timing")
|
|
<< "&utc=" << category
|
|
<< "&utv=" << variable
|
|
<< "&utt=" << milliseconds
|
|
<< "&utl=" << label;
|
|
|
|
postOrGet(defaultThreadPool, RobloxGoogleAnalytics::kGoogleAnalyticsBaseURL, params.str(), Http::kContentTypeDefaultUnspecified, sync, false, true);
|
|
|
|
analyticTrackedSignalVar();
|
|
}
|
|
|
|
rbx::signal<void()>& analyticTrackedSignal()
|
|
{
|
|
return analyticTrackedSignalVar;
|
|
}
|
|
|
|
void setExperimentVariation(const std::string& name, int value)
|
|
{
|
|
std::stringstream s;
|
|
s << "&experiment=" << name << "&variation=" << value;
|
|
experimentString = s.str();
|
|
}
|
|
|
|
const std::string& getSessionId()
|
|
{
|
|
return googleClientID;
|
|
}
|
|
|
|
} // namespace GoogleAnalytics
|
|
|
|
namespace InfluxDb {
|
|
|
|
int r = 10000; // throttle
|
|
|
|
std::size_t hash_value(const Point& p)
|
|
{
|
|
return boost::hash<std::string>()(p.name);
|
|
}
|
|
|
|
void init()
|
|
{
|
|
r = rand() % 10000;
|
|
}
|
|
|
|
bool canSend(int throttleHundredthsPercentage)
|
|
{
|
|
return r < throttleHundredthsPercentage && !DFString::HttpInfluxURL.empty();
|
|
}
|
|
|
|
std::string escapeTag(std::string tag)
|
|
{
|
|
std::string s = tag;
|
|
boost::replace_all(s, " ", "\\ ");
|
|
boost::replace_all(s, ",", "\\,");
|
|
return s;
|
|
}
|
|
|
|
using namespace RBX::Analytics::InfluxDb;
|
|
|
|
void reportPointsV2(const std::string& resource, const boost::unordered_set<Point>& points, int throttleHundredthsPercentage, bool blocking, const std::string& userIdOverride)
|
|
{
|
|
if (!canSend(throttleHundredthsPercentage))
|
|
return;
|
|
|
|
RBXASSERT(!points.empty());
|
|
|
|
// Pushes to InfluxDB
|
|
std::string url = DFString::HttpInfluxURL + "/write?db=" + DFString::HttpInfluxDatabase + "&u=" + DFString::HttpInfluxUser + "&p=" + DFString::HttpInfluxPassword;
|
|
|
|
// format: https://docs.influxdata.com/influxdb/v0.10/write_protocols/line/
|
|
// <measurement>[,<tag-key>=<tag-value>...] <field-key>=<field-value>[,<field2-key>=<field2-value>...] [unix-nano-timestamp]
|
|
|
|
// convert all guest IDs to -1
|
|
std::string uid = (userIdOverride.empty() ? (!userId.empty() ? userId.c_str() : "0") : userIdOverride.c_str());
|
|
if (atoi(uid.c_str()) < 0)
|
|
uid = "-1";
|
|
|
|
// tags
|
|
std::stringstream ss;
|
|
ss << RBX::format("%s,placeid=%s,userid=%s,reporter=%s,platform=%s,device=%s,appversion=%s,location=%s,osversion=%s ",
|
|
resource.c_str(),
|
|
(!placeId.empty() ? placeId.c_str() : "0"),
|
|
uid.c_str(),
|
|
escapeTag(reporter).c_str(),
|
|
escapeTag(DebugSettings::singleton().osPlatform()).c_str(),
|
|
escapeTag(DebugSettings::singleton().deviceName()).c_str(),
|
|
escapeTag(appVersion).c_str(),
|
|
escapeTag(location).c_str(),
|
|
escapeTag(DebugSettings::singleton().osVer()).c_str());
|
|
|
|
// fields
|
|
boost::unordered_set<Point>::const_iterator it = points.begin();
|
|
for (int index = 0; points.end() != it; ++it, ++index)
|
|
{
|
|
if (index > 0)
|
|
ss << ',';
|
|
ss << it->name << '=' << it->json;
|
|
}
|
|
|
|
postOrGet(defaultThreadPool, url, ss.str(), "", blocking, false, true);
|
|
}
|
|
|
|
void reportPoints(const std::string& resource, const boost::unordered_set<Point>& points, int throttleHundredthsPercentage, bool blocking, const std::string& userIdOverride)
|
|
{
|
|
if (DFFlag::InfluxDb09Enabled)
|
|
{
|
|
reportPointsV2(resource, points, throttleHundredthsPercentage, blocking, userIdOverride);
|
|
return;
|
|
}
|
|
|
|
if (!canSend(throttleHundredthsPercentage))
|
|
return;
|
|
|
|
RBXASSERT(!points.empty());
|
|
|
|
// Pushes to InfluxDB.
|
|
std::string url = DFString::HttpInfluxURL + "/db/" + DFString::HttpInfluxDatabase + "/series?u=" + DFString::HttpInfluxUser + "&p=" + Http::urlEncode(DFString::HttpInfluxPassword);
|
|
|
|
std::string json = "[{";
|
|
json += "\"name\":\"" + resource + "\",";
|
|
json += "\"columns\":[\"placeid\",\"platform\",\"device\",\"osversion\",\"appversion\",\"userid\",\"reporter\",\"location\"";
|
|
for (boost::unordered_set<Point>::const_iterator it = points.begin(); points.end() != it; ++it)
|
|
{
|
|
json += ",\"" + it->name + "\"";
|
|
}
|
|
json += "],";
|
|
|
|
// two levels are here because we are actually sending a list of several points,
|
|
// even though this function only handles one row of points at a time
|
|
json += "\"points\":[[";
|
|
json += (!placeId.empty() ? placeId : "0");
|
|
json += ",\"" + DebugSettings::singleton().osPlatform() + "\"";
|
|
json += ",\"" + DebugSettings::singleton().deviceName() + "\"";
|
|
json += ",\"" + DebugSettings::singleton().osVer() + "\"";
|
|
json += ",\"" + appVersion + "\"";
|
|
|
|
if (!userIdOverride.empty())
|
|
json += "," + userIdOverride;
|
|
else
|
|
json += "," + (!userId.empty() ? userId : "0");
|
|
|
|
json += ",\"" + reporter + "\"";
|
|
json += ",\"" + location + "\"";
|
|
for (boost::unordered_set<Point>::const_iterator it = points.begin(); points.end() != it; ++it)
|
|
{
|
|
json += "," + it->json;
|
|
}
|
|
json += "]]";
|
|
|
|
json += "}]";
|
|
|
|
postOrGet(defaultThreadPool, url, json, Http::kContentTypeApplicationJson, blocking, false, true);
|
|
}
|
|
|
|
} // namespace InfluxDb
|
|
} // namespace ExternalAnalytics
|
|
} // namespace RBX
|