Program Listing for File Telemetry.cpp

Return to documentation for file (src/flamegpu/io/Telemetry.cpp)

#include "flamegpu/io/Telemetry.h"

#include <cstdlib>
#include <cerrno>
#include <algorithm>
#include <memory>
#include <array>
#include <iostream>
#include <cstdio>
#include <cctype>
#include <sstream>

#include "flamegpu/version.h"

namespace flamegpu {
namespace io {

namespace {
    // the FLAMEGPU2 telemetry app id.
    const char TELEMETRY_APP_ID[] = "94AC5E3F-F674-4E29-BF87-DAF4BA7F8F79";

    // Flag tracking if telemetry is enabled, initialised subject to preproc and env vars in initialiseFromEnvironmentIfNeeded
    static bool enabled = false;
    // Flag indicating if users have suppressed telemetry warnings
    static bool suppressed = false;
    // Flag indicating if FLAMEGPU telemetry test mode is enabled.
    static bool testMode = false;
    // Flag indicating if these anon namespace statics have been enabled or not
    static bool initialised = false;
    // Flag indicating if the user has been notified / encouraged to enable usage statistics or not
    static bool haveNotified = false;

    bool cmakeStrToBool(const char * input) {
        // Assume truthy
        bool rtn = true;
        if (input != NULL) {
            std::string s = std::string(input);
            // Trim leading whitespace
            s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
            // Trim trailing whitespace
            s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());

            // Transform the input to lower case
            std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return static_cast<unsigned char>(std::tolower(c)); });
            // If it's a falsey option, set it to falesy.
            if (s == "0" || s == "false" || s == "off") {
                rtn = false;
            }
        }
        return rtn;
    }

    /*
     * Initialise namespace scoped static variables based from environment variables, if not done already.
     This is done in a method to avoid potential UB with initialisation order of static members and the system environment, which maybe undefined based on SO posts / is not just a direct mapping.
    */
    void initialiseFromEnvironmentIfNeeded() {
        if (!initialised) {
            // Enabled by default
            enabled = true;
            // If the sharing environment var is set, parse it following cmake boolean logic
            char * env_FLAMEGPU_SHARE_USAGE_STATISTICS = std::getenv("FLAMEGPU_SHARE_USAGE_STATISTICS");
            if (env_FLAMEGPU_SHARE_USAGE_STATISTICS != NULL) {
                enabled = cmakeStrToBool(env_FLAMEGPU_SHARE_USAGE_STATISTICS);
            } else {
                // if the environment variable is not specified, use the value from the preprocessor
                #ifdef FLAMEGPU_SHARE_USAGE_STATISTICS
                    enabled = true;
                #else
                    enabled = false;
                #endif
            }
            // Parse env and cmake variables to find the default value for suppression.
            suppressed = false;
            char * env_FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE = std::getenv("FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE");
            if (env_FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE != NULL) {
                suppressed = cmakeStrToBool(env_FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE);
            } else {
                // if the environment variable is not specified, use the value from the preprocessor
                #ifdef FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE
                    suppressed = true;
                #else
                    suppressed = false;
                #endif
            }
            // Parse env and cmake variables to find the default value for test dev mode. .
            testMode = false;
            char * env_FLAMEGPU_TELEMETRY_TEST_MODE = std::getenv("FLAMEGPU_TELEMETRY_TEST_MODE");
            if (env_FLAMEGPU_TELEMETRY_TEST_MODE != NULL) {
                testMode = cmakeStrToBool(env_FLAMEGPU_TELEMETRY_TEST_MODE);
            } else {
                // if the environment variable is not specified, use the value from the preprocessor
                #ifdef FLAMEGPU_TELEMETRY_TEST_MODE
                    testMode = true;
                #else
                    testMode = false;
                #endif
            }
            // Mark this as initialised.
            initialised = true;
        }
    }

}  // anonymous namespace

void Telemetry::enable() {
    // Initialise from the env var is needed. A ctor might be nicer.
    initialiseFromEnvironmentIfNeeded();
    // set to enabled.
    enabled = true;
}

void Telemetry::disable() {
    // initialise from the env var if needed
    initialiseFromEnvironmentIfNeeded();
    // set to disabled.
    enabled = false;
}

bool Telemetry::isEnabled() {
    // initialise from the env var if needed
    initialiseFromEnvironmentIfNeeded();
    // return if it is enabled or not.
    return enabled;
}


void Telemetry::suppressNotice() {
    // initialise from the env var if needed
    initialiseFromEnvironmentIfNeeded();
    // set the suppressed flag in the anon namespace
    suppressed = true;
}

bool Telemetry::isTestMode() {
    // initialise from the env var if needed
    initialiseFromEnvironmentIfNeeded();
    // return if it is enabled or not.
    return testMode;
}


std::string Telemetry::generateData(std::string event_name, std::map<std::string, std::string> payload_items, bool isSWIG) {
    // Initialise from the env var is needed. A ctor might be nicer.
    initialiseFromEnvironmentIfNeeded();

    const std::string var_testmode = "$TEST_MODE";
    const std::string var_appID = "$APP_ID";
    const std::string var_telemetryRandomID = "$TELEMETRY_RANDOM_ID";
    const std::string var_eventName = "$EVENT_TYPE";
    const std::string var_payload = "$PAYLOAD";

    // 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;

    // 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.
    if (isSWIG) {
        std::string py_version = "pyflamegpu" + std::string(flamegpu::VERSION_STRING);
        payload_items["appVersion"] = py_version;  // e.g. 'pyflamegpu2.0.0-alpha.3' (graphed in Telemetry deck)
    } else {
        payload_items["appVersion"] = flamegpu::VERSION_STRING;  // e.g. '2.0.0-alpha.3' (graphed in Telemetry deck)
    }

    // other version strings
    payload_items["appVersionFull"] = flamegpu::VERSION_FULL;
    payload_items["majorSystemVersion"] = std::to_string(flamegpu::VERSION_MAJOR);  // e.g. '2' (graphed in Telemetry deck)
    std::string major_minor_patch = std::to_string(flamegpu::VERSION_MAJOR) + "." + std::to_string(flamegpu::VERSION_MINOR) + "." + std::to_string(flamegpu::VERSION_PATCH);
    payload_items["majorMinorSystemVersion"] = major_minor_patch;  // e.g. '2.0.0' (graphed in Telemetry deck)
    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)

    // OS
#ifdef _WIN32
    payload_items["operatingSystem"] = "Windows";
#elif __linux__
    payload_items["operatingSystem"] = "Linux";
#elif __unix__
    payload_items["operatingSystem"] = "Unix";
#else
    payload_items["operatingSystem"] = "Other";
#endif
    // visualiastion status
#ifdef FLAMEGPU_VISUALISATION
    payload_items["Visualisation"] = "true";
#else
    payload_items["Visualisation"] = "false";
#endif

    // create the payload
    std::string payload = "";
    bool first = true;
    // iterate payload to generate the payload array
    for (const auto& [key, value] : payload_items) {
        std::string item_str;
        if (!first)
            item_str = ",\"" + key + ":" + value + "\"";
        else
            item_str = "\"" + key + ":" + value + "\"";
        payload.append(item_str);
        first = false;
    }

    // create the telemetry json package
    std::string telemetry_data = R"json(
    [{
        "isTestMode": "$TEST_MODE",
        "appID": "$APP_ID",
        "clientUser": "$TELEMETRY_RANDOM_ID",
        "sessionID": "",
        "type" : "$EVENT_TYPE",
        "payload" : [$PAYLOAD]
    }])json";
    // update the placeholders
    telemetry_data.replace(telemetry_data.find(var_testmode), var_testmode.length(), testmode);
    telemetry_data.replace(telemetry_data.find(var_appID), var_appID.length(), appID);
    telemetry_data.replace(telemetry_data.find(var_telemetryRandomID), var_telemetryRandomID.length(), telemetryRandomID);
    telemetry_data.replace(telemetry_data.find(var_eventName), var_eventName.length(), event_name);
    telemetry_data.replace(telemetry_data.find(var_payload), var_payload.length(), payload);
    // Remove newlines and replace with space
    telemetry_data.erase(std::remove(telemetry_data.begin(), telemetry_data.end(), '\n'), telemetry_data.end());
    // Remove tabs and replace with space
    telemetry_data.erase(std::remove(telemetry_data.begin(), telemetry_data.end(), '\t'), telemetry_data.end());
     // Remove spaces
    telemetry_data.erase(std::remove_if(telemetry_data.begin(), telemetry_data.end(), [](char c) {
            return std::isspace(static_cast<unsigned char>(c));
        }), telemetry_data.end());
    // Use escape characters
    size_t pos = 0;
    while ((pos = telemetry_data.find("\"", pos)) != std::string::npos) {
        telemetry_data.replace(pos, 1, "\\\"");
        pos += 2;
    }
    return telemetry_data;
}

bool Telemetry::sendData(std::string telemetry_data) {
    // Initialise from the env var is needed. A ctor might be nicer.
    initialiseFromEnvironmentIfNeeded();

    // Maximum duration curl to attempt to connect to the endpoint
    const float CURL_CONNECT_TIMEOUT = 0.5;
    // Maximum total duration for the curl call, including connection and payload
    const float CURL_MAX_TIME = 1.0;
    // Silent curl command (-s) and redirect response output to null
    std::string null;
#if _WIN32
    null = "nul";
#else
    null = "/dev/null";
#endif
    std::stringstream curl_command;
    curl_command << "curl";
    curl_command << " -s";
    curl_command << " -o " << null;
    curl_command << " --connect-timeout " << std::to_string(CURL_CONNECT_TIMEOUT);
    curl_command << " --max-time " << std::to_string(CURL_MAX_TIME);
    curl_command << " -X POST \"" << std::string(TELEMETRY_ENDPOINT) << "\"";
    curl_command << " -H \"Content-Type: application/json; charset=utf-8\"";
    curl_command << " --data-raw \"" << telemetry_data + "\"";
    curl_command << " > " << null << " 2>&1";
    // capture the return value
    if (std::system(curl_command.str().c_str()) != EXIT_SUCCESS) {
        return false;
    }

    return true;
}

void Telemetry::encourageUsage() {
    // Only print the usage encouragement notice if it has not already been encouraged, telemetry is not enabled and telemetry has not been suppressed.
    if (!haveNotified && !suppressed && !isEnabled()) {
        fprintf(stdout,
            "NOTICE: The FLAME GPU software is reliant on evidence to support is continued development. Please "
            "consider enabling FLAMEGPU_SHARE_USAGE_STATISTICS during CMake configuration, "
            "setting FLAMEGPU_SHARE_USAGE_STATISTICS to true as an environment variable, "
            "or setting the Simulation/Ensemble config telemetry property to true.\n"
            "This message can be silenced by suppressing all output (--quiet), "
            "calling flamegpu::io::Telemetry::suppressNotice, or defining a system environment variable FLAMEGPU_TELEMETRY_SUPPRESS_NOTICE\n");
        // Set the flag that this has already been emitted once during execution of the current binary file, so it doesn't happen again.
        haveNotified = true;
    }
}

}  // namespace io
}  // namespace flamegpu