Skip to content

Commit

Permalink
Improvements to Telemetry (#1252)
Browse files Browse the repository at this point in the history
Telemetry: Switch to ID per user rather than per build directory

This improves accuracy of unique user counting, especially for python usrs.

---------

Co-authored-by: Peter Heywood <[email protected]>
  • Loading branch information
mondus and ptheywood authored Nov 21, 2024
1 parent c347616 commit fa4caae
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ Information is collected when a simulation, ensemble or test suite run have comp
The [TelemetryDeck](https://telemetrydeck.com/) service is used to store telemetry data.
All data is sent to their API endpoint of https://nom.telemetrydeck.com/v1/ via https. For more details please review the [TelmetryDeck privacy policy](https://telemetrydeck.com/privacy/).

We do not collect any personal data such as usernames, email addresses or machine identifiers.
We do not collect any personal data such as usernames, email addresses or hardware identifiers but we do generate a random user identifier. This identifier is salted and hashed by Telemetry deck.

More information can be found in the [FLAMEGPU documentation](https://docs.flamegpu.com/guide/telemetry).

Expand Down
18 changes: 18 additions & 0 deletions include/flamegpu/io/Telemetry.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ static void encourageUsage();
* @return if telemetry is currently in test mode or not.
*/
static bool isTestMode();

/**
* Gets a cross platform location for storing user configuration data. Required to store a per user Id. On windows this uses the AppData folder (CSIDL_APPDATA) and in Linux this uses XDG_CONFIG_HOME.
* @return Root directory for storing configuration files
*/
static std::string getConfigDirectory();

/**
* Generates a randomised 36 character alphanumeric string for use as a User Id.
* @return A 36 character randomised alphanumeric string
*/
static std::string generateRandomId();

/**
* Obtains a unique user Id. If a configuration file (i.e. ${XDG_CONFIG_HOME}/flamegpu/telemetry_user.cfg on linux) exists this will be loaded from disk otherwise it will be generated and stored in the configuration location. If the configuration location is not writeable a new user Id will be generated each time. The user Id will be further obfuscated by Telemetry Deck which will salt and hash the Id.
* @return A 36 character randomised alphanumeric string representing a unique user Id
*/
static std::string getUserId();
};
} // namespace io
} // namespace flamegpu
Expand Down
104 changes: 103 additions & 1 deletion src/flamegpu/io/Telemetry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@
#include <memory>
#include <array>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <cstdio>
#include <cctype>
#include <sstream>
#include <string>
#include <map>
#include <random>
#ifdef _WIN32
#include <windows.h>
#include <shlobj.h>
#else
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#endif

#include "flamegpu/version.h"

Expand Down Expand Up @@ -159,7 +170,7 @@ std::string Telemetry::generateData(std::string event_name, std::map<std::string
// check ENV for test variable FLAMEGPU_TEST_ENVIRONMENT
std::string testmode = isTestMode() ? "true" : "false";
std::string appID = TELEMETRY_APP_ID;
std::string telemetryRandomID = flamegpu::TELEMETRY_RANDOM_ID;
std::string telemetryRandomID = Telemetry::getUserId(); // Obtain a unique user ID

// Differentiate pyflamegpu in the payload via the SWIG compiler macro, which we only define when building for pyflamegpu.
// A user could potentially static link against a build using that macro, but that's not a use-case we are currently concerned with.
Expand All @@ -178,6 +189,7 @@ std::string Telemetry::generateData(std::string event_name, std::map<std::string
payload_items["appVersionPatch"] = std::to_string(flamegpu::VERSION_PATCH);
payload_items["appVersionPreRelease"] = flamegpu::VERSION_PRERELEASE;
payload_items["buildNumber"] = flamegpu::VERSION_BUILDMETADATA; // e.g. '0553592f' (graphed in Telemetry deck)
payload_items["buildID"] = flamegpu::TELEMETRY_RANDOM_ID; // e.g. 'e7e0fe30325c83a3ad52e2cb2180e50979d3e6bcb0692789977a06ad09889843' (ID generated a cmake time)

// OS
#ifdef _WIN32
Expand Down Expand Up @@ -291,5 +303,95 @@ void Telemetry::encourageUsage() {
}
}

std::string Telemetry::getConfigDirectory() {
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
return std::string(path);
}
throw std::runtime_error("Unable to retrieve config directory on Windows");
#else
const char* configHome = std::getenv("XDG_CONFIG_HOME");
if (configHome) {
return std::string(configHome);
}
const char* home = std::getenv("HOME");
if (home) {
return std::string(home) + "/.config";
}
// try and get the user directory if home is not set
struct passwd pwd;
struct passwd* result = nullptr;
char buffer[4096];
int ret = getpwuid_r(getuid(), &pwd, buffer, sizeof(buffer), &result);
if (ret == 0 && result != nullptr) {
return std::string(pwd.pw_dir) + "/.config";
}

throw std::runtime_error("Unable to retrieve config directory on Linux");
#endif
}

std::string Telemetry::generateRandomId() {
const char charset[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
const size_t length = 36;
std::random_device rd;
std::mt19937 generator(rd());
std::uniform_int_distribution<size_t> distribution(0, sizeof(charset) - 2);

std::string randomId;
for (size_t i = 0; i < length; ++i) {
randomId += charset[distribution(generator)];
}
return randomId;
}

std::string Telemetry::getUserId() {
// Generate and a new random 36-character user ID
std::string userId = Telemetry::generateRandomId();

// Try to load an existing ID from file if it exists
try {
// Determine config file location
std::string configDir = Telemetry::getConfigDirectory() + "/flamegpu";
std::filesystem::create_directories(configDir); // Ensure the directory exists
std::string filePath = configDir + "/telemetry_user.cfg";

// Check if the file exists
if (std::filesystem::exists(filePath)) {
std::ifstream file(filePath, std::ios_base::binary);
if (file.is_open()) {
std::string cached_id;
std::getline(file, cached_id); // overwrite existing Id
file.close();
// Config file and user id found so return it if not empty (either because file is externally modified or file is a directory)
if (!cached_id.empty())
return cached_id;
else
return userId;
}
else {
throw std::runtime_error("Unable to open user ID file for reading");
}
}

std::ofstream file(filePath, std::ios_base::binary);
if (file.is_open()) {
file << userId;
file.close();
}
else {
throw std::runtime_error("Unable to create user ID file");
}
} catch (const std::exception&) {
fprintf(stderr, "Warning: Telemetry User Id file is not read/writeable from config file. A new User Id will be used.\n");
}
return userId;
}


} // namespace io
} // namespace flamegpu

0 comments on commit fa4caae

Please sign in to comment.