Merge pull request #2897 from bunnei/telemetry-ui

Telemetry UI and final touches
This commit is contained in:
bunnei 2017-08-26 20:15:15 -04:00 committed by GitHub
commit 22fc378fe9
20 changed files with 446 additions and 48 deletions

View file

@ -165,6 +165,8 @@ int main(int argc, char** argv) {
break; // Expected case
}
Core::Telemetry().AddField(Telemetry::FieldType::App, "Frontend", "SDL");
while (emu_window->IsOpen()) {
system.RunLoop();
}

View file

@ -156,8 +156,12 @@ void Config::ReadValues() {
static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689));
// Web Service
Settings::values.enable_telemetry =
sdl2_config->GetBoolean("WebService", "enable_telemetry", true);
Settings::values.telemetry_endpoint_url = sdl2_config->Get(
"WebService", "telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry");
Settings::values.citra_username = sdl2_config->Get("WebService", "citra_username", "");
Settings::values.citra_token = sdl2_config->Get("WebService", "citra_token", "");
}
void Config::Reload() {

View file

@ -176,7 +176,14 @@ use_gdbstub=false
gdbstub_port=24689
[WebService]
# Whether or not to enable telemetry
# 0: No, 1 (default): Yes
enable_telemetry =
# Endpoint URL for submitting telemetry data
telemetry_endpoint_url =
telemetry_endpoint_url = https://services.citra-emu.org/api/telemetry
# Username and token for Citra Web Service
# See https://services.citra-emu.org/ for more info
citra_username =
citra_token =
)";
}

View file

@ -12,6 +12,7 @@ set(SRCS
configuration/configure_graphics.cpp
configuration/configure_input.cpp
configuration/configure_system.cpp
configuration/configure_web.cpp
debugger/graphics/graphics.cpp
debugger/graphics/graphics_breakpoint_observer.cpp
debugger/graphics/graphics_breakpoints.cpp
@ -42,6 +43,7 @@ set(HEADERS
configuration/configure_graphics.h
configuration/configure_input.h
configuration/configure_system.h
configuration/configure_web.h
debugger/graphics/graphics.h
debugger/graphics/graphics_breakpoint_observer.h
debugger/graphics/graphics_breakpoints.h
@ -71,6 +73,7 @@ set(UIS
configuration/configure_graphics.ui
configuration/configure_input.ui
configuration/configure_system.ui
configuration/configure_web.ui
debugger/registers.ui
hotkeys.ui
main.ui

View file

@ -139,10 +139,13 @@ void Config::ReadValues() {
qt_config->endGroup();
qt_config->beginGroup("WebService");
Settings::values.enable_telemetry = qt_config->value("enable_telemetry", true).toBool();
Settings::values.telemetry_endpoint_url =
qt_config->value("telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry")
.toString()
.toStdString();
Settings::values.citra_username = qt_config->value("citra_username").toString().toStdString();
Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString();
qt_config->endGroup();
qt_config->beginGroup("UI");
@ -194,6 +197,7 @@ void Config::ReadValues() {
UISettings::values.show_status_bar = qt_config->value("showStatusBar", true).toBool();
UISettings::values.confirm_before_closing = qt_config->value("confirmClose", true).toBool();
UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
UISettings::values.callout_flags = qt_config->value("calloutFlags", 0).toUInt();
qt_config->endGroup();
}
@ -283,8 +287,11 @@ void Config::SaveValues() {
qt_config->endGroup();
qt_config->beginGroup("WebService");
qt_config->setValue("enable_telemetry", Settings::values.enable_telemetry);
qt_config->setValue("telemetry_endpoint_url",
QString::fromStdString(Settings::values.telemetry_endpoint_url));
qt_config->setValue("citra_username", QString::fromStdString(Settings::values.citra_username));
qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token));
qt_config->endGroup();
qt_config->beginGroup("UI");
@ -320,6 +327,7 @@ void Config::SaveValues() {
qt_config->setValue("showStatusBar", UISettings::values.show_status_bar);
qt_config->setValue("confirmClose", UISettings::values.confirm_before_closing);
qt_config->setValue("firstStart", UISettings::values.first_start);
qt_config->setValue("calloutFlags", UISettings::values.callout_flags);
qt_config->endGroup();
}

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>441</width>
<height>501</height>
<width>740</width>
<height>500</height>
</rect>
</property>
<property name="windowTitle">
@ -49,6 +49,11 @@
<string>Debug</string>
</attribute>
</widget>
<widget class="ConfigureWeb" name="webTab">
<attribute name="title">
<string>Web</string>
</attribute>
</widget>
</widget>
</item>
<item>
@ -97,6 +102,12 @@
<header>configuration/configure_graphics.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ConfigureWeb</class>
<extends>QWidget</extends>
<header>configuration/configure_web.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>

View file

@ -23,5 +23,6 @@ void ConfigureDialog::applyConfiguration() {
ui->graphicsTab->applyConfiguration();
ui->audioTab->applyConfiguration();
ui->debugTab->applyConfiguration();
ui->webTab->applyConfiguration();
Settings::Apply();
}

View file

@ -0,0 +1,52 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include "citra_qt/configuration/configure_web.h"
#include "core/settings.h"
#include "core/telemetry_session.h"
#include "ui_configure_web.h"
ConfigureWeb::ConfigureWeb(QWidget* parent)
: QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) {
ui->setupUi(this);
connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this,
&ConfigureWeb::refreshTelemetryID);
this->setConfiguration();
}
ConfigureWeb::~ConfigureWeb() {}
void ConfigureWeb::setConfiguration() {
ui->web_credentials_disclaimer->setWordWrap(true);
ui->telemetry_learn_more->setOpenExternalLinks(true);
ui->telemetry_learn_more->setText("<a "
"href='https://citra-emu.org/entry/"
"telemetry-and-why-thats-a-good-thing/'>Learn more</a>");
ui->web_signup_link->setOpenExternalLinks(true);
ui->web_signup_link->setText("<a href='https://services.citra-emu.org/'>Sign up</a>");
ui->web_token_info_link->setOpenExternalLinks(true);
ui->web_token_info_link->setText(
"<a href='https://citra-emu.org/wiki/citra-web-service/'>What is my token?</a>");
ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry);
ui->edit_username->setText(QString::fromStdString(Settings::values.citra_username));
ui->edit_token->setText(QString::fromStdString(Settings::values.citra_token));
ui->label_telemetry_id->setText("Telemetry ID: 0x" +
QString::number(Core::GetTelemetryId(), 16).toUpper());
}
void ConfigureWeb::applyConfiguration() {
Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked();
Settings::values.citra_username = ui->edit_username->text().toStdString();
Settings::values.citra_token = ui->edit_token->text().toStdString();
Settings::Apply();
}
void ConfigureWeb::refreshTelemetryID() {
const u64 new_telemetry_id{Core::RegenerateTelemetryId()};
ui->label_telemetry_id->setText("Telemetry ID: 0x" +
QString::number(new_telemetry_id, 16).toUpper());
}

View file

@ -0,0 +1,30 @@
// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include <QWidget>
namespace Ui {
class ConfigureWeb;
}
class ConfigureWeb : public QWidget {
Q_OBJECT
public:
explicit ConfigureWeb(QWidget* parent = nullptr);
~ConfigureWeb();
void applyConfiguration();
public slots:
void refreshTelemetryID();
private:
void setConfiguration();
std::unique_ptr<Ui::ConfigureWeb> ui;
};

View file

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConfigureWeb</class>
<widget class="QWidget" name="ConfigureWeb">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QGroupBox" name="groupBoxWebConfig">
<property name="title">
<string>Citra Web Service</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutCitraWebService">
<item>
<widget class="QLabel" name="web_credentials_disclaimer">
<property name="text">
<string>By providing your username and token, you agree to allow Citra to collect additional usage data, which may include user identifying information.</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayoutCitraUsername">
<item row="0" column="0">
<widget class="QLabel" name="label_username">
<property name="text">
<string>Username: </string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="edit_username">
<property name="maxLength">
<number>36</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_token">
<property name="text">
<string>Token: </string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="edit_token">
<property name="maxLength">
<number>36</number>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="web_signup_link">
<property name="text">
<string>Sign up</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="web_token_info_link">
<property name="text">
<string>What is my token?</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Telemetry</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="toggle_telemetry">
<property name="text">
<string>Share anonymous usage data with the Citra team</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="telemetry_learn_more">
<property name="text">
<string>Learn more</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayoutTelemetryId">
<item row="0" column="0">
<widget class="QLabel" name="label_telemetry_id">
<property name="text">
<string>Telemetry ID:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="button_regenerate_telemetry_id">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Regenerate</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -48,6 +48,47 @@
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
#endif
/**
* "Callouts" are one-time instructional messages shown to the user. In the config settings, there
* is a bitfield "callout_flags" options, used to track if a message has already been shown to the
* user. This is 32-bits - if we have more than 32 callouts, we should retire and recyle old ones.
*/
enum class CalloutFlag : uint32_t {
Telemetry = 0x1,
};
static void ShowCalloutMessage(const QString& message, CalloutFlag flag) {
if (UISettings::values.callout_flags & static_cast<uint32_t>(flag)) {
return;
}
UISettings::values.callout_flags |= static_cast<uint32_t>(flag);
QMessageBox msg;
msg.setText(message);
msg.setStandardButtons(QMessageBox::Ok);
msg.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
msg.setStyleSheet("QLabel{min-width: 900px;}");
msg.exec();
}
void GMainWindow::ShowCallouts() {
static const QString telemetry_message =
tr("To help improve Citra, the Citra Team collects anonymous usage data. No private or "
"personally identifying information is collected. This data helps us to understand how "
"people use Citra and prioritize our efforts. Furthermore, it helps us to more easily "
"identify emulation bugs and performance issues. This data includes:<ul><li>Information"
" about the version of Citra you are using</li><li>Performance data about the games you "
"play</li><li>Your configuration settings</li><li>Information about your computer "
"hardware</li><li>Emulation errors and crash information</li></ul>By default, this "
"feature is enabled. To disable this feature, click 'Emulation' from the menu and then "
"select 'Configure...'. Then, on the 'Web' tab, uncheck 'Share anonymous usage data with"
" the Citra team'. <br/><br/>By using this software, you agree to the above terms.<br/>"
"<br/><a href='https://citra-emu.org/entry/telemetry-and-why-thats-a-good-thing/'>Learn "
"more</a>");
ShowCalloutMessage(telemetry_message, CalloutFlag::Telemetry);
}
GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
Pica::g_debug_context = Pica::DebugContext::Construct();
setAcceptDrops(true);
@ -73,6 +114,9 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
UpdateUITheme();
// Show one-time "callout" messages to the user
ShowCallouts();
QStringList args = QApplication::arguments();
if (args.length() >= 2) {
BootGame(args[1]);
@ -320,6 +364,8 @@ bool GMainWindow::LoadROM(const QString& filename) {
const Core::System::ResultStatus result{system.Load(render_window, filename.toStdString())};
Core::Telemetry().AddField(Telemetry::FieldType::App, "Frontend", "Qt");
if (result != Core::System::ResultStatus::Success) {
switch (result) {
case Core::System::ResultStatus::ErrorGetLoader:

View file

@ -80,6 +80,8 @@ private:
void BootGame(const QString& filename);
void ShutdownGame();
void ShowCallouts();
/**
* Stores the filename in the recently loaded files list.
* The new filename is stored at the beginning of the recently loaded files list.

View file

@ -48,6 +48,8 @@ struct Values {
// Shortcut name <Shortcut, context>
std::vector<Shortcut> shortcuts;
uint32_t callout_flags;
};
extern Values values;

View file

@ -130,7 +130,10 @@ struct Values {
u16 gdbstub_port;
// WebService
bool enable_telemetry;
std::string telemetry_endpoint_url;
std::string citra_username;
std::string citra_token;
} extern values;
// a special value for Values::region_value indicating that citra will automatically select a region

View file

@ -3,8 +3,10 @@
// Refer to the license.txt file included.
#include <cstring>
#include <cryptopp/osrng.h>
#include "common/assert.h"
#include "common/file_util.h"
#include "common/scm_rev.h"
#include "common/x64/cpu_detect.h"
#include "core/core.h"
@ -29,12 +31,65 @@ static const char* CpuVendorToStr(Common::CPUVendor vendor) {
UNREACHABLE();
}
static u64 GenerateTelemetryId() {
u64 telemetry_id{};
CryptoPP::AutoSeededRandomPool rng;
rng.GenerateBlock(reinterpret_cast<CryptoPP::byte*>(&telemetry_id), sizeof(u64));
return telemetry_id;
}
u64 GetTelemetryId() {
u64 telemetry_id{};
static const std::string& filename{FileUtil::GetUserPath(D_CONFIG_IDX) + "telemetry_id"};
if (FileUtil::Exists(filename)) {
FileUtil::IOFile file(filename, "rb");
if (!file.IsOpen()) {
LOG_ERROR(Core, "failed to open telemetry_id: %s", filename.c_str());
return {};
}
file.ReadBytes(&telemetry_id, sizeof(u64));
} else {
FileUtil::IOFile file(filename, "wb");
if (!file.IsOpen()) {
LOG_ERROR(Core, "failed to open telemetry_id: %s", filename.c_str());
return {};
}
telemetry_id = GenerateTelemetryId();
file.WriteBytes(&telemetry_id, sizeof(u64));
}
return telemetry_id;
}
u64 RegenerateTelemetryId() {
const u64 new_telemetry_id{GenerateTelemetryId()};
static const std::string& filename{FileUtil::GetUserPath(D_CONFIG_IDX) + "telemetry_id"};
FileUtil::IOFile file(filename, "wb");
if (!file.IsOpen()) {
LOG_ERROR(Core, "failed to open telemetry_id: %s", filename.c_str());
return {};
}
file.WriteBytes(&new_telemetry_id, sizeof(u64));
return new_telemetry_id;
}
TelemetrySession::TelemetrySession() {
#ifdef ENABLE_WEB_SERVICE
backend = std::make_unique<WebService::TelemetryJson>();
if (Settings::values.enable_telemetry) {
backend = std::make_unique<WebService::TelemetryJson>(
Settings::values.telemetry_endpoint_url, Settings::values.citra_username,
Settings::values.citra_token);
} else {
backend = std::make_unique<Telemetry::NullVisitor>();
}
#else
backend = std::make_unique<Telemetry::NullVisitor>();
#endif
// Log one-time top-level information
AddField(Telemetry::FieldType::None, "TelemetryId", GetTelemetryId());
// Log one-time session start information
const s64 init_time{std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())

View file

@ -35,4 +35,16 @@ private:
std::unique_ptr<Telemetry::VisitorInterface> backend; ///< Backend interface that logs fields
};
/**
* Gets TelemetryId, a unique identifier used for the user's telemetry sessions.
* @returns The current TelemetryId for the session.
*/
u64 GetTelemetryId();
/**
* Regenerates TelemetryId, a unique identifier used for the user's telemetry sessions.
* @returns The new TelemetryId that was generated.
*/
u64 RegenerateTelemetryId();
} // namespace Core

View file

@ -3,7 +3,6 @@
// Refer to the license.txt file included.
#include "common/assert.h"
#include "core/settings.h"
#include "web_service/telemetry_json.h"
#include "web_service/web_backend.h"
@ -81,7 +80,7 @@ void TelemetryJson::Complete() {
SerializeSection(Telemetry::FieldType::UserFeedback, "UserFeedback");
SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig");
SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem");
PostJson(Settings::values.telemetry_endpoint_url, TopSection().dump());
PostJson(endpoint_url, TopSection().dump(), true, username, token);
}
} // namespace WebService

View file

@ -17,7 +17,9 @@ namespace WebService {
*/
class TelemetryJson : public Telemetry::VisitorInterface {
public:
TelemetryJson() = default;
TelemetryJson(const std::string& endpoint_url, const std::string& username,
const std::string& token)
: endpoint_url(endpoint_url), username(username), token(token) {}
~TelemetryJson() = default;
void Visit(const Telemetry::Field<bool>& field) override;
@ -49,6 +51,9 @@ private:
nlohmann::json output;
std::array<nlohmann::json, 7> sections;
std::string endpoint_url;
std::string username;
std::string token;
};
} // namespace WebService

View file

@ -2,51 +2,62 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#ifdef _WIN32
#include <winsock.h>
#endif
#include <cstdlib>
#include <thread>
#include <cpr/cpr.h>
#include <stdlib.h>
#include "common/logging/log.h"
#include "web_service/web_backend.h"
namespace WebService {
static constexpr char API_VERSION[]{"1"};
static constexpr char ENV_VAR_USERNAME[]{"CITRA_WEB_SERVICES_USERNAME"};
static constexpr char ENV_VAR_TOKEN[]{"CITRA_WEB_SERVICES_TOKEN"};
static std::string GetEnvironmentVariable(const char* name) {
const char* value{getenv(name)};
if (value) {
return value;
}
return {};
}
static std::unique_ptr<cpr::Session> g_session;
const std::string& GetUsername() {
static const std::string username{GetEnvironmentVariable(ENV_VAR_USERNAME)};
return username;
}
const std::string& GetToken() {
static const std::string token{GetEnvironmentVariable(ENV_VAR_TOKEN)};
return token;
}
void PostJson(const std::string& url, const std::string& data) {
void PostJson(const std::string& url, const std::string& data, bool allow_anonymous,
const std::string& username, const std::string& token) {
if (url.empty()) {
LOG_ERROR(WebService, "URL is invalid");
return;
}
if (GetUsername().empty() || GetToken().empty()) {
LOG_ERROR(WebService, "Environment variables %s and %s must be set to POST JSON",
ENV_VAR_USERNAME, ENV_VAR_TOKEN);
const bool are_credentials_provided{!token.empty() && !username.empty()};
if (!allow_anonymous && !are_credentials_provided) {
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
return;
}
cpr::PostAsync(cpr::Url{url}, cpr::Body{data}, cpr::Header{{"Content-Type", "application/json"},
{"x-username", GetUsername()},
{"x-token", GetToken()},
{"api-version", API_VERSION}});
#ifdef _WIN32
// On Windows, CPR/libcurl does not properly initialize Winsock. The below code is used to
// initialize Winsock globally, which fixes this problem. Without this, only the first CPR
// session will properly be created, and subsequent ones will fail.
WSADATA wsa_data;
const int wsa_result{WSAStartup(MAKEWORD(2, 2), &wsa_data)};
if (wsa_result) {
LOG_CRITICAL(WebService, "WSAStartup failed: %d", wsa_result);
}
#endif
// Built request header
cpr::Header header;
if (are_credentials_provided) {
// Authenticated request if credentials are provided
header = {{"Content-Type", "application/json"},
{"x-username", username.c_str()},
{"x-token", token.c_str()},
{"api-version", API_VERSION}};
} else {
// Otherwise, anonymous request
header = cpr::Header{{"Content-Type", "application/json"}, {"api-version", API_VERSION}};
}
// Post JSON asynchronously
static cpr::AsyncResponse future;
future = cpr::PostAsync(cpr::Url{url.c_str()}, cpr::Body{data.c_str()}, header);
}
} // namespace WebService

View file

@ -9,23 +9,15 @@
namespace WebService {
/**
* Gets the current username for accessing services.citra-emu.org.
* @returns Username as a string, empty if not set.
*/
const std::string& GetUsername();
/**
* Gets the current token for accessing services.citra-emu.org.
* @returns Token as a string, empty if not set.
*/
const std::string& GetToken();
/**
* Posts JSON to services.citra-emu.org.
* @param url URL of the services.citra-emu.org endpoint to post data to.
* @param data String of JSON data to use for the body of the POST request.
* @param allow_anonymous If true, allow anonymous unauthenticated requests.
* @param username Citra username to use for authentication.
* @param token Citra token to use for authentication.
*/
void PostJson(const std::string& url, const std::string& data);
void PostJson(const std::string& url, const std::string& data, bool allow_anonymous,
const std::string& username = {}, const std::string& token = {});
} // namespace WebService