diff --git a/.travis/common/travis-ci.env b/.travis/common/travis-ci.env
index a333786ce127047455e5489e45f4a3d6317de7aa..ec8e2dd635bfbdcc47b1e62e8331347110460174 100644
--- a/.travis/common/travis-ci.env
+++ b/.travis/common/travis-ci.env
@@ -10,3 +10,7 @@ TRAVIS_JOB_ID
 TRAVIS_JOB_NUMBER
 TRAVIS_REPO_SLUG
 TRAVIS_TAG
+
+# yuzu specific flags
+ENABLE_COMPATIBILITY_REPORTING
+USE_DISCORD_PRESENCE
diff --git a/.travis/linux/build.sh b/.travis/linux/build.sh
index c3a1f93e40b5a2f836bc40fe4959439751ce317f..2fced727d798a3141513dd57eabb18f5e0ae05b0 100755
--- a/.travis/linux/build.sh
+++ b/.travis/linux/build.sh
@@ -1,4 +1,4 @@
 #!/bin/bash -ex
 
 mkdir -p "$HOME/.ccache"
-docker run --env-file .travis/common/travis-ci.env -v $(pwd):/yuzu -v "$HOME/.ccache":/root/.ccache ubuntu:18.04 /bin/bash /yuzu/.travis/linux/docker.sh
+docker run -e ENABLE_COMPATIBILITY_REPORTING --env-file .travis/common/travis-ci.env -v $(pwd):/yuzu -v "$HOME/.ccache":/root/.ccache ubuntu:18.04 /bin/bash /yuzu/.travis/linux/docker.sh
diff --git a/.travis/linux/docker.sh b/.travis/linux/docker.sh
index 892d2480add625ac4c8fc9473eef7586b88fba8e..4fe3326f99eaf5ee88f59c71f9509fbb83fef0f2 100755
--- a/.travis/linux/docker.sh
+++ b/.travis/linux/docker.sh
@@ -6,7 +6,7 @@ apt-get install --no-install-recommends -y build-essential git libqt5opengl5-dev
 cd /yuzu
 
 mkdir build && cd build
-cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -G Ninja
+cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -G Ninja
 ninja
 
 ccache -s
diff --git a/.travis/macos/build.sh b/.travis/macos/build.sh
index e68dc14002691613010f9f25bfcdd9f25b41f2b9..dce12099b5afdaff363d93744c666307e4d092f3 100755
--- a/.travis/macos/build.sh
+++ b/.travis/macos/build.sh
@@ -9,7 +9,7 @@ export PATH="/usr/local/opt/ccache/libexec:$PATH"
 
 mkdir build && cd build
 cmake --version
-cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON
+cmake .. -DYUZU_USE_BUNDLED_UNICORN=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DUSE_DISCORD_PRESENCE=ON
 make -j4
 
 ccache -s
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cd990188e7a82c7d9c9d1e3f7b0e08d98babba92..04744370461cfa9e92619a64ffe82e693bfd47cb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,10 +15,14 @@ CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_SDL2 "Download bundled SDL2 binaries" ON
 option(ENABLE_QT "Enable the Qt frontend" ON)
 CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" ON "ENABLE_QT;MSVC" OFF)
 
+option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON)
+
 option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON)
 
 option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
 
+option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
+
 if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit)
     message(STATUS "Copying pre-commit hook")
     file(COPY hooks/pre-commit
@@ -321,6 +325,13 @@ if (ENABLE_QT)
     find_package(Qt5 REQUIRED COMPONENTS Widgets OpenGL ${QT_PREFIX_HINT})
 endif()
 
+if (ENABLE_WEB_SERVICE)
+    add_definitions(-DENABLE_WEB_SERVICE)
+endif()
+if (YUZU_ENABLE_COMPATIBILITY_REPORTING)
+    add_definitions(-DYUZU_ENABLE_COMPATIBILITY_REPORTING)
+endif()
+
 # Platform-specific library requirements
 # ======================================
 
diff --git a/appveyor.yml b/appveyor.yml
index d475816ab7c0a4e2008bdbf5e6ff9d826d393e92..6d0e6522a5f71f1de27b9addcb4d0f461246516b 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -39,11 +39,12 @@ before_build:
   - mkdir %BUILD_TYPE%_build
   - cd %BUILD_TYPE%_build
   - ps: |
+        $COMPAT = if ($env:ENABLE_COMPATIBILITY_REPORTING -eq $null) {0} else {$env:ENABLE_COMPATIBILITY_REPORTING}
         if ($env:BUILD_TYPE -eq 'msvc') {
           # redirect stderr and change the exit code to prevent powershell from cancelling the build if cmake prints a warning
-          cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1 && exit 0'
+          cmd /C 'cmake -G "Visual Studio 15 2017 Win64" -DYUZU_USE_BUNDLED_QT=1 -DYUZU_USE_BUNDLED_SDL2=1 -DYUZU_USE_BUNDLED_UNICORN=1 -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DUSE_DISCORD_PRESENCE=ON .. 2>&1 && exit 0'
         } else {
-          C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DYUZU_BUILD_UNICORN=1 -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON .. 2>&1"
+          C:\msys64\usr\bin\bash.exe -lc "cmake -G 'MSYS Makefiles' -DYUZU_BUILD_UNICORN=1 -DCMAKE_BUILD_TYPE=Release -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DYUZU_ENABLE_COMPATIBILITY_REPORTING=${COMPAT} -DUSE_DISCORD_PRESENCE=ON .. 2>&1"
         }
   - cd ..
 
diff --git a/externals/discord-rpc b/externals/discord-rpc
new file mode 160000
index 0000000000000000000000000000000000000000..e32d001809c4aad56cef2a5321b54442d830174f
--- /dev/null
+++ b/externals/discord-rpc
@@ -0,0 +1 @@
+Subproject commit e32d001809c4aad56cef2a5321b54442d830174f
diff --git a/externals/libressl b/externals/libressl
new file mode 160000
index 0000000000000000000000000000000000000000..7d01cb01cb1a926ecb4c9c98b107ef3c26f59dfb
--- /dev/null
+++ b/externals/libressl
@@ -0,0 +1 @@
+Subproject commit 7d01cb01cb1a926ecb4c9c98b107ef3c26f59dfb
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a88551fbc87a400697554e1725d4b45d8bfa534c..f69d00a2b2b8cb66de89e7913d0aa566262652e6 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -13,3 +13,6 @@ endif()
 if (ENABLE_QT)
     add_subdirectory(yuzu)
 endif()
+if (ENABLE_WEB_SERVICE)
+    add_subdirectory(web_service)
+endif()
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 6a3f1fe0856c846f5a84dfede62613dc6537b637..8985e4367f78624062c0e2c7f1f7baabb4893d83 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -41,6 +41,8 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/scm_rev.cpp.in" "${CMAKE_CURRENT_SOU
 add_library(common STATIC
     alignment.h
     assert.h
+    detached_tasks.cpp
+    detached_tasks.h
     bit_field.h
     bit_set.h
     cityhash.cpp
@@ -87,6 +89,7 @@ add_library(common STATIC
     timer.cpp
     timer.h
     vector_math.h
+    web_result.h
 )
 
 if(ARCHITECTURE_x86_64)
diff --git a/src/common/detached_tasks.cpp b/src/common/detached_tasks.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a347d9e02a95fc0b6b54be4c17b496f915c71f2b
--- /dev/null
+++ b/src/common/detached_tasks.cpp
@@ -0,0 +1,41 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <thread>
+#include "common/assert.h"
+#include "common/detached_tasks.h"
+
+namespace Common {
+
+DetachedTasks* DetachedTasks::instance = nullptr;
+
+DetachedTasks::DetachedTasks() {
+    ASSERT(instance == nullptr);
+    instance = this;
+}
+
+void DetachedTasks::WaitForAllTasks() {
+    std::unique_lock<std::mutex> lock(mutex);
+    cv.wait(lock, [this]() { return count == 0; });
+}
+
+DetachedTasks::~DetachedTasks() {
+    std::unique_lock<std::mutex> lock(mutex);
+    ASSERT(count == 0);
+    instance = nullptr;
+}
+
+void DetachedTasks::AddTask(std::function<void()> task) {
+    std::unique_lock<std::mutex> lock(instance->mutex);
+    ++instance->count;
+    std::thread([task{std::move(task)}]() {
+        task();
+        std::unique_lock<std::mutex> lock(instance->mutex);
+        --instance->count;
+        std::notify_all_at_thread_exit(instance->cv, std::move(lock));
+    })
+        .detach();
+}
+
+} // namespace Common
diff --git a/src/common/detached_tasks.h b/src/common/detached_tasks.h
new file mode 100644
index 0000000000000000000000000000000000000000..eae27788d0b6924082712b5c81f45dbc8f713f27
--- /dev/null
+++ b/src/common/detached_tasks.h
@@ -0,0 +1,39 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+#include <condition_variable>
+#include <functional>
+
+namespace Common {
+
+/**
+ * A background manager which ensures that all detached task is finished before program exits.
+ *
+ * Some tasks, telemetry submission for example, prefer executing asynchronously and don't care
+ * about the result. These tasks are suitable for std::thread::detach(). However, this is unsafe if
+ * the task is launched just before the program exits (which is a common case for telemetry), so we
+ * need to block on these tasks on program exit.
+ *
+ * To make detached task safe, a single DetachedTasks object should be placed in the main(), and
+ * call WaitForAllTasks() after all program execution but before global/static variable destruction.
+ * Any potentially unsafe detached task should be executed via DetachedTasks::AddTask.
+ */
+class DetachedTasks {
+public:
+    DetachedTasks();
+    ~DetachedTasks();
+    void WaitForAllTasks();
+
+    static void AddTask(std::function<void()> task);
+
+private:
+    static DetachedTasks* instance;
+
+    std::condition_variable cv;
+    std::mutex mutex;
+    int count = 0;
+};
+
+} // namespace Common
diff --git a/src/common/web_result.h b/src/common/web_result.h
new file mode 100644
index 0000000000000000000000000000000000000000..13610a7eaddbcab12f8f856fb2ae07d65c53cb65
--- /dev/null
+++ b/src/common/web_result.h
@@ -0,0 +1,24 @@
+// Copyright 2018 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <string>
+
+namespace Common {
+struct WebResult {
+    enum class Code : u32 {
+        Success,
+        InvalidURL,
+        CredentialsMissing,
+        LibError,
+        HttpError,
+        WrongContent,
+        NoWebservice,
+    };
+    Code result_code;
+    std::string result_string;
+    std::string returned_data;
+};
+} // namespace Commo
\ No newline at end of file
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 23fd6e920b5fc279ce99d44ca5f53013e02f00c4..95f8b5d4a82cfb5a1d1ef6094d17360c3cfa440b 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -394,6 +394,9 @@ create_target_directory_groups(core)
 
 target_link_libraries(core PUBLIC common PRIVATE audio_core video_core)
 target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static mbedtls opus unicorn open_source_archives)
+if (ENABLE_WEB_SERVICE)
+    target_link_libraries(core PUBLIC json-headers web_service)
+endif()
 
 if (ARCHITECTURE_x86_64)
     target_sources(core PRIVATE
diff --git a/src/core/settings.h b/src/core/settings.h
index 0318d019c84ab69001c0ccfd72263fcd80417b26..1808f5937d9a96fc6bdc6a567c15b8ed3d885acd 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -155,6 +155,12 @@ struct Values {
     // Debugging
     bool use_gdbstub;
     u16 gdbstub_port;
+
+    // WebService
+    bool enable_telemetry;
+    std::string web_api_url;
+    std::string yuzu_username;
+    std::string yuzu_token;
 } extern values;
 
 void Apply();
diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp
index b0df154ca15cf4b19b86e59e83459541367aeb55..09c85297ad6db227d452dfd5a6dd0ed9dae0be28 100644
--- a/src/core/telemetry_session.cpp
+++ b/src/core/telemetry_session.cpp
@@ -6,6 +6,8 @@
 #include "common/common_types.h"
 #include "common/file_util.h"
 
+#include <mbedtls/ctr_drbg.h>
+#include <mbedtls/entropy.h>
 #include "core/core.h"
 #include "core/file_sys/control_metadata.h"
 #include "core/file_sys/patch_manager.h"
@@ -13,10 +15,30 @@
 #include "core/settings.h"
 #include "core/telemetry_session.h"
 
+#ifdef ENABLE_WEB_SERVICE
+#include "web_service/telemetry_json.h"
+#include "web_service/verify_login.h"
+#endif
+
 namespace Core {
 
 static u64 GenerateTelemetryId() {
     u64 telemetry_id{};
+
+    mbedtls_entropy_context entropy;
+    mbedtls_entropy_init(&entropy);
+    mbedtls_ctr_drbg_context ctr_drbg;
+    const char* personalization = "yuzu Telemetry ID";
+
+    mbedtls_ctr_drbg_init(&ctr_drbg);
+    mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
+                          (const unsigned char*)personalization, strlen(personalization));
+    ASSERT(mbedtls_ctr_drbg_random(&ctr_drbg, reinterpret_cast<unsigned char*>(&telemetry_id),
+                                   sizeof(u64)) == 0);
+
+    mbedtls_ctr_drbg_free(&ctr_drbg);
+    mbedtls_entropy_free(&entropy);
+
     return telemetry_id;
 }
 
@@ -25,14 +47,21 @@ u64 GetTelemetryId() {
     const std::string filename{FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) +
                                "telemetry_id"};
 
-    if (FileUtil::Exists(filename)) {
+    bool generate_new_id = !FileUtil::Exists(filename);
+    if (!generate_new_id) {
         FileUtil::IOFile file(filename, "rb");
         if (!file.IsOpen()) {
             LOG_ERROR(Core, "failed to open telemetry_id: {}", filename);
             return {};
         }
         file.ReadBytes(&telemetry_id, sizeof(u64));
-    } else {
+        if (telemetry_id == 0) {
+            LOG_ERROR(Frontend, "telemetry_id is 0. Generating a new one.", telemetry_id);
+            generate_new_id = true;
+        }
+    }
+
+    if (generate_new_id) {
         FileUtil::IOFile file(filename, "wb");
         if (!file.IsOpen()) {
             LOG_ERROR(Core, "failed to open telemetry_id: {}", filename);
@@ -59,23 +88,20 @@ u64 RegenerateTelemetryId() {
     return new_telemetry_id;
 }
 
-std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func) {
+bool VerifyLogin(std::string username, std::string token) {
 #ifdef ENABLE_WEB_SERVICE
-    return WebService::VerifyLogin(username, token, Settings::values.verify_endpoint_url, func);
+    return WebService::VerifyLogin(Settings::values.web_api_url, username, token);
 #else
-    return std::async(std::launch::async, [func{std::move(func)}]() {
-        func();
-        return false;
-    });
+    return false;
 #endif
 }
 
 TelemetrySession::TelemetrySession() {
 #ifdef ENABLE_WEB_SERVICE
     if (Settings::values.enable_telemetry) {
-        backend = std::make_unique<WebService::TelemetryJson>(
-            Settings::values.telemetry_endpoint_url, Settings::values.yuzu_username,
-            Settings::values.yuzu_token);
+        backend = std::make_unique<WebService::TelemetryJson>(Settings::values.web_api_url,
+                                                              Settings::values.yuzu_username,
+                                                              Settings::values.yuzu_token);
     } else {
         backend = std::make_unique<Telemetry::NullVisitor>();
     }
@@ -94,7 +120,8 @@ TelemetrySession::TelemetrySession() {
     u64 program_id{};
     const Loader::ResultStatus res{System::GetInstance().GetAppLoader().ReadProgramId(program_id)};
     if (res == Loader::ResultStatus::Success) {
-        AddField(Telemetry::FieldType::Session, "ProgramId", program_id);
+        std::string formatted_program_id{fmt::format("{:016X}", program_id)};
+        AddField(Telemetry::FieldType::Session, "ProgramId", formatted_program_id);
 
         std::string name;
         System::GetInstance().GetAppLoader().ReadTitle(name);
diff --git a/src/core/telemetry_session.h b/src/core/telemetry_session.h
index dbc4f8bd49a882146efd68ab32d70d02fb14127d..e6976ad45ba47e4cd36311d12d07cbb74c19990a 100644
--- a/src/core/telemetry_session.h
+++ b/src/core/telemetry_session.h
@@ -4,7 +4,6 @@
 
 #pragma once
 
-#include <future>
 #include <memory>
 #include "common/telemetry.h"
 
@@ -31,6 +30,8 @@ public:
         field_collection.AddField(type, name, std::move(value));
     }
 
+    static void FinalizeAsyncJob();
+
 private:
     Telemetry::FieldCollection field_collection; ///< Tracks all added fields for the session
     std::unique_ptr<Telemetry::VisitorInterface> backend; ///< Backend interface that logs fields
@@ -55,6 +56,6 @@ u64 RegenerateTelemetryId();
  * @param func A function that gets exectued when the verification is finished
  * @returns Future with bool indicating whether the verification succeeded
  */
-std::future<bool> VerifyLogin(std::string username, std::string token, std::function<void()> func);
+bool VerifyLogin(std::string username, std::string token);
 
 } // namespace Core
diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ef77728c0113d95a5458ebd8b4bf38f949f521f0
--- /dev/null
+++ b/src/web_service/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_library(web_service STATIC
+    telemetry_json.cpp
+    telemetry_json.h
+    verify_login.cpp
+    verify_login.h
+    web_backend.cpp
+    web_backend.h
+)
+
+create_target_directory_groups(web_service)
+
+get_directory_property(OPENSSL_LIBS
+        DIRECTORY ${CMAKE_SOURCE_DIR}/externals/libressl
+        DEFINITION OPENSSL_LIBS)
+add_definitions(-DCPPHTTPLIB_OPENSSL_SUPPORT)
+target_link_libraries(web_service PUBLIC common json-headers ${OPENSSL_LIBS} httplib lurlparser)
diff --git a/src/web_service/json.h b/src/web_service/json.h
new file mode 100644
index 0000000000000000000000000000000000000000..88b31501ed961182ffd17ecbdf99d657e8045189
--- /dev/null
+++ b/src/web_service/json.h
@@ -0,0 +1,18 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+// This hack is needed to support json.hpp on platforms where the C++17 stdlib
+// lacks std::string_view. See https://github.com/nlohmann/json/issues/735.
+// clang-format off
+#if !__has_include(<string_view>) && __has_include(<experimental/string_view>)
+# include <experimental/string_view>
+# define string_view experimental::string_view
+# include <json.hpp>
+# undef string_view
+#else
+# include <json.hpp>
+#endif
+// clang-format on
diff --git a/src/web_service/telemetry_json.cpp b/src/web_service/telemetry_json.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a0b7f9c4ea4312fd97d63746f06cd12f806b155d
--- /dev/null
+++ b/src/web_service/telemetry_json.cpp
@@ -0,0 +1,94 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <thread>
+#include "common/assert.h"
+#include "common/detached_tasks.h"
+#include "web_service/telemetry_json.h"
+#include "web_service/web_backend.h"
+
+namespace WebService {
+
+template <class T>
+void TelemetryJson::Serialize(Telemetry::FieldType type, const std::string& name, T value) {
+    sections[static_cast<u8>(type)][name] = value;
+}
+
+void TelemetryJson::SerializeSection(Telemetry::FieldType type, const std::string& name) {
+    TopSection()[name] = sections[static_cast<unsigned>(type)];
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<bool>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<double>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<float>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<u8>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<u16>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<u32>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<u64>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<s8>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<s16>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<s32>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<s64>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<std::string>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue());
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<const char*>& field) {
+    Serialize(field.GetType(), field.GetName(), std::string(field.GetValue()));
+}
+
+void TelemetryJson::Visit(const Telemetry::Field<std::chrono::microseconds>& field) {
+    Serialize(field.GetType(), field.GetName(), field.GetValue().count());
+}
+
+void TelemetryJson::Complete() {
+    SerializeSection(Telemetry::FieldType::App, "App");
+    SerializeSection(Telemetry::FieldType::Session, "Session");
+    SerializeSection(Telemetry::FieldType::Performance, "Performance");
+    SerializeSection(Telemetry::FieldType::UserFeedback, "UserFeedback");
+    SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig");
+    SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem");
+
+    auto content = TopSection().dump();
+    // Send the telemetry async but don't handle the errors since they were written to the log
+    Common::DetachedTasks::AddTask(
+        [host{this->host}, username{this->username}, token{this->token}, content]() {
+            Client{host, username, token}.PostJson("/telemetry", content, true);
+        });
+}
+
+} // namespace WebService
diff --git a/src/web_service/telemetry_json.h b/src/web_service/telemetry_json.h
new file mode 100644
index 0000000000000000000000000000000000000000..9bc886538bbb3697f4d6b0facc948143c7a663f3
--- /dev/null
+++ b/src/web_service/telemetry_json.h
@@ -0,0 +1,59 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <string>
+#include "common/telemetry.h"
+#include "common/web_result.h"
+#include "web_service/json.h"
+
+namespace WebService {
+
+/**
+ * Implementation of VisitorInterface that serialized telemetry into JSON, and submits it to the
+ * yuzu web service
+ */
+class TelemetryJson : public Telemetry::VisitorInterface {
+public:
+    TelemetryJson(const std::string& host, const std::string& username, const std::string& token)
+        : host(host), username(username), token(token) {}
+    ~TelemetryJson() = default;
+
+    void Visit(const Telemetry::Field<bool>& field) override;
+    void Visit(const Telemetry::Field<double>& field) override;
+    void Visit(const Telemetry::Field<float>& field) override;
+    void Visit(const Telemetry::Field<u8>& field) override;
+    void Visit(const Telemetry::Field<u16>& field) override;
+    void Visit(const Telemetry::Field<u32>& field) override;
+    void Visit(const Telemetry::Field<u64>& field) override;
+    void Visit(const Telemetry::Field<s8>& field) override;
+    void Visit(const Telemetry::Field<s16>& field) override;
+    void Visit(const Telemetry::Field<s32>& field) override;
+    void Visit(const Telemetry::Field<s64>& field) override;
+    void Visit(const Telemetry::Field<std::string>& field) override;
+    void Visit(const Telemetry::Field<const char*>& field) override;
+    void Visit(const Telemetry::Field<std::chrono::microseconds>& field) override;
+
+    void Complete() override;
+
+private:
+    nlohmann::json& TopSection() {
+        return sections[static_cast<u8>(Telemetry::FieldType::None)];
+    }
+
+    template <class T>
+    void Serialize(Telemetry::FieldType type, const std::string& name, T value);
+
+    void SerializeSection(Telemetry::FieldType type, const std::string& name);
+
+    nlohmann::json output;
+    std::array<nlohmann::json, 7> sections;
+    std::string host;
+    std::string username;
+    std::string token;
+};
+
+} // namespace WebService
diff --git a/src/web_service/verify_login.cpp b/src/web_service/verify_login.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..02e1b74f3cc9a15c783b3ff409e6fa832318b921
--- /dev/null
+++ b/src/web_service/verify_login.cpp
@@ -0,0 +1,27 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "web_service/json.h"
+#include "web_service/verify_login.h"
+#include "web_service/web_backend.h"
+
+namespace WebService {
+
+bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token) {
+    Client client(host, username, token);
+    auto reply = client.GetJson("/profile", false).returned_data;
+    if (reply.empty()) {
+        return false;
+    }
+    nlohmann::json json = nlohmann::json::parse(reply);
+    const auto iter = json.find("username");
+
+    if (iter == json.end()) {
+        return username.empty();
+    }
+
+    return username == *iter;
+}
+
+} // namespace WebService
diff --git a/src/web_service/verify_login.h b/src/web_service/verify_login.h
new file mode 100644
index 0000000000000000000000000000000000000000..39db32dbbace89555cf72361d128287667a86e19
--- /dev/null
+++ b/src/web_service/verify_login.h
@@ -0,0 +1,22 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <future>
+#include <string>
+
+namespace WebService {
+
+/**
+ * Checks if username and token is valid
+ * @param host the web API URL
+ * @param username yuzu username to use for authentication.
+ * @param token yuzu token to use for authentication.
+ * @returns a bool indicating whether the verification succeeded
+ */
+bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token);
+
+} // namespace WebService
diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a726fb8eb5c3ace423d7f0718b2da77f165a7573
--- /dev/null
+++ b/src/web_service/web_backend.cpp
@@ -0,0 +1,147 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <cstdlib>
+#include <string>
+#include <thread>
+#include <LUrlParser.h>
+#include "common/logging/log.h"
+#include "common/web_result.h"
+#include "core/settings.h"
+#include "web_service/web_backend.h"
+
+namespace WebService {
+
+static constexpr char API_VERSION[]{"1"};
+
+constexpr int HTTP_PORT = 80;
+constexpr int HTTPS_PORT = 443;
+
+constexpr int TIMEOUT_SECONDS = 30;
+
+Client::JWTCache Client::jwt_cache{};
+
+Client::Client(const std::string& host, const std::string& username, const std::string& token)
+    : host(host), username(username), token(token) {
+    if (username == jwt_cache.username && token == jwt_cache.token) {
+        jwt = jwt_cache.jwt;
+    }
+}
+
+Common::WebResult Client::GenericJson(const std::string& method, const std::string& path,
+                                      const std::string& data, const std::string& jwt,
+                                      const std::string& username, const std::string& token) {
+    if (cli == nullptr) {
+        auto parsedUrl = LUrlParser::clParseURL::ParseURL(host);
+        int port;
+        if (parsedUrl.m_Scheme == "http") {
+            if (!parsedUrl.GetPort(&port)) {
+                port = HTTP_PORT;
+            }
+            cli =
+                std::make_unique<httplib::Client>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS);
+        } else if (parsedUrl.m_Scheme == "https") {
+            if (!parsedUrl.GetPort(&port)) {
+                port = HTTPS_PORT;
+            }
+            cli = std::make_unique<httplib::SSLClient>(parsedUrl.m_Host.c_str(), port,
+                                                       TIMEOUT_SECONDS);
+        } else {
+            LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme);
+            return Common::WebResult{Common::WebResult::Code::InvalidURL, "Bad URL scheme"};
+        }
+    }
+    if (cli == nullptr) {
+        LOG_ERROR(WebService, "Invalid URL {}", host + path);
+        return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"};
+    }
+
+    httplib::Headers params;
+    if (!jwt.empty()) {
+        params = {
+            {std::string("Authorization"), fmt::format("Bearer {}", jwt)},
+        };
+    } else if (!username.empty()) {
+        params = {
+            {std::string("x-username"), username},
+            {std::string("x-token"), token},
+        };
+    }
+
+    params.emplace(std::string("api-version"), std::string(API_VERSION));
+    if (method != "GET") {
+        params.emplace(std::string("Content-Type"), std::string("application/json"));
+    };
+
+    httplib::Request request;
+    request.method = method;
+    request.path = path;
+    request.headers = params;
+    request.body = data;
+
+    httplib::Response response;
+
+    if (!cli->send(request, response)) {
+        LOG_ERROR(WebService, "{} to {} returned null", method, host + path);
+        return Common::WebResult{Common::WebResult::Code::LibError, "Null response"};
+    }
+
+    if (response.status >= 400) {
+        LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path,
+                  response.status);
+        return Common::WebResult{Common::WebResult::Code::HttpError,
+                                 std::to_string(response.status)};
+    }
+
+    auto content_type = response.headers.find("content-type");
+
+    if (content_type == response.headers.end()) {
+        LOG_ERROR(WebService, "{} to {} returned no content", method, host + path);
+        return Common::WebResult{Common::WebResult::Code::WrongContent, ""};
+    }
+
+    if (content_type->second.find("application/json") == std::string::npos &&
+        content_type->second.find("text/html; charset=utf-8") == std::string::npos) {
+        LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path,
+                  content_type->second);
+        return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"};
+    }
+    return Common::WebResult{Common::WebResult::Code::Success, "", response.body};
+}
+
+void Client::UpdateJWT() {
+    if (!username.empty() && !token.empty()) {
+        auto result = GenericJson("POST", "/jwt/internal", "", "", username, token);
+        if (result.result_code != Common::WebResult::Code::Success) {
+            LOG_ERROR(WebService, "UpdateJWT failed");
+        } else {
+            jwt_cache.username = username;
+            jwt_cache.token = token;
+            jwt_cache.jwt = jwt = result.returned_data;
+        }
+    }
+}
+
+Common::WebResult Client::GenericJson(const std::string& method, const std::string& path,
+                                      const std::string& data, bool allow_anonymous) {
+    if (jwt.empty()) {
+        UpdateJWT();
+    }
+
+    if (jwt.empty() && !allow_anonymous) {
+        LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
+        return Common::WebResult{Common::WebResult::Code::CredentialsMissing, "Credentials needed"};
+    }
+
+    auto result = GenericJson(method, path, data, jwt);
+    if (result.result_string == "401") {
+        // Try again with new JWT
+        UpdateJWT();
+        result = GenericJson(method, path, data, jwt);
+    }
+
+    return result;
+}
+
+} // namespace WebService
diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h
new file mode 100644
index 0000000000000000000000000000000000000000..549bcce291d5d8d64a1d25a625c7f9b9d9894ac8
--- /dev/null
+++ b/src/web_service/web_backend.h
@@ -0,0 +1,91 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <future>
+#include <string>
+#include <tuple>
+#include <httplib.h>
+#include "common/common_types.h"
+#include "common/web_result.h"
+
+namespace httplib {
+class Client;
+}
+
+namespace WebService {
+
+class Client {
+public:
+    Client(const std::string& host, const std::string& username, const std::string& token);
+
+    /**
+     * Posts JSON to the specified path.
+     * @param path the URL segment after the host address.
+     * @param data String of JSON data to use for the body of the POST request.
+     * @param allow_anonymous If true, allow anonymous unauthenticated requests.
+     * @return the result of the request.
+     */
+    Common::WebResult PostJson(const std::string& path, const std::string& data,
+                               bool allow_anonymous) {
+        return GenericJson("POST", path, data, allow_anonymous);
+    }
+
+    /**
+     * Gets JSON from the specified path.
+     * @param path the URL segment after the host address.
+     * @param allow_anonymous If true, allow anonymous unauthenticated requests.
+     * @return the result of the request.
+     */
+    Common::WebResult GetJson(const std::string& path, bool allow_anonymous) {
+        return GenericJson("GET", path, "", allow_anonymous);
+    }
+
+    /**
+     * Deletes JSON to the specified path.
+     * @param path the URL segment after the host address.
+     * @param data String of JSON data to use for the body of the DELETE request.
+     * @param allow_anonymous If true, allow anonymous unauthenticated requests.
+     * @return the result of the request.
+     */
+    Common::WebResult DeleteJson(const std::string& path, const std::string& data,
+                                 bool allow_anonymous) {
+        return GenericJson("DELETE", path, data, allow_anonymous);
+    }
+
+private:
+    /// A generic function handles POST, GET and DELETE request together
+    Common::WebResult GenericJson(const std::string& method, const std::string& path,
+                                  const std::string& data, bool allow_anonymous);
+
+    /**
+     * A generic function with explicit authentication method specified
+     * JWT is used if the jwt parameter is not empty
+     * username + token is used if jwt is empty but username and token are not empty
+     * anonymous if all of jwt, username and token are empty
+     */
+    Common::WebResult GenericJson(const std::string& method, const std::string& path,
+                                  const std::string& data, const std::string& jwt = "",
+                                  const std::string& username = "", const std::string& token = "");
+
+    // Retrieve a new JWT from given username and token
+    void UpdateJWT();
+
+    std::string host;
+    std::string username;
+    std::string token;
+    std::string jwt;
+    std::unique_ptr<httplib::Client> cli;
+
+    struct JWTCache {
+        std::string username;
+        std::string token;
+        std::string jwt;
+    };
+    static JWTCache jwt_cache;
+};
+
+} // namespace WebService
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index f48b69809b202e08612cd719e1332d406521a1eb..f93ba2569ab897f1f13ac398ea468f7bb58d9a20 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -29,6 +29,8 @@ add_executable(yuzu
     configuration/configure_input.h
     configuration/configure_system.cpp
     configuration/configure_system.h
+    configuration/configure_web.cpp
+    configuration/configure_web.h
     debugger/graphics/graphics_breakpoint_observer.cpp
     debugger/graphics/graphics_breakpoint_observer.h
     debugger/graphics/graphics_breakpoints.cpp
@@ -42,6 +44,7 @@ add_executable(yuzu
     debugger/profiler.h
     debugger/wait_tree.cpp
     debugger/wait_tree.h
+    discord.h
     game_list.cpp
     game_list.h
     game_list_p.h
@@ -57,6 +60,8 @@ add_executable(yuzu
     util/spinbox.h
     util/util.cpp
     util/util.h
+    compatdb.cpp
+    compatdb.h
     yuzu.rc
 )
 
@@ -70,8 +75,10 @@ set(UIS
     configuration/configure_graphics.ui
     configuration/configure_input.ui
     configuration/configure_system.ui
+    configuration/configure_web.ui
     hotkeys.ui
     main.ui
+    compatdb.ui
 )
 
 file(GLOB COMPAT_LIST
@@ -113,6 +120,15 @@ target_link_libraries(yuzu PRIVATE common core input_common video_core)
 target_link_libraries(yuzu PRIVATE Boost::boost glad Qt5::OpenGL Qt5::Widgets)
 target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
 
+if (USE_DISCORD_PRESENCE)
+    target_sources(yuzu PUBLIC
+        discord_impl.cpp
+        discord_impl.h
+    )
+    target_link_libraries(yuzu PRIVATE discord-rpc)
+    target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE)
+endif()
+
 if(UNIX AND NOT APPLE)
     install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
 endif()
diff --git a/src/yuzu/compatdb.cpp b/src/yuzu/compatdb.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..45f8b4461bfc7d741ec13f3883da63c3f22cb8ca
--- /dev/null
+++ b/src/yuzu/compatdb.cpp
@@ -0,0 +1,61 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QButtonGroup>
+#include <QMessageBox>
+#include <QPushButton>
+#include "common/logging/log.h"
+#include "common/telemetry.h"
+#include "core/core.h"
+#include "core/telemetry_session.h"
+#include "ui_compatdb.h"
+#include "yuzu/compatdb.h"
+
+CompatDB::CompatDB(QWidget* parent)
+    : QWizard(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+      ui{std::make_unique<Ui::CompatDB>()} {
+    ui->setupUi(this);
+    connect(ui->radioButton_Perfect, &QRadioButton::clicked, this, &CompatDB::EnableNext);
+    connect(ui->radioButton_Great, &QRadioButton::clicked, this, &CompatDB::EnableNext);
+    connect(ui->radioButton_Okay, &QRadioButton::clicked, this, &CompatDB::EnableNext);
+    connect(ui->radioButton_Bad, &QRadioButton::clicked, this, &CompatDB::EnableNext);
+    connect(ui->radioButton_IntroMenu, &QRadioButton::clicked, this, &CompatDB::EnableNext);
+    connect(ui->radioButton_WontBoot, &QRadioButton::clicked, this, &CompatDB::EnableNext);
+    connect(button(NextButton), &QPushButton::clicked, this, &CompatDB::Submit);
+}
+
+CompatDB::~CompatDB() = default;
+
+enum class CompatDBPage { Intro = 0, Selection = 1, Final = 2 };
+
+void CompatDB::Submit() {
+    QButtonGroup* compatibility = new QButtonGroup(this);
+    compatibility->addButton(ui->radioButton_Perfect, 0);
+    compatibility->addButton(ui->radioButton_Great, 1);
+    compatibility->addButton(ui->radioButton_Okay, 2);
+    compatibility->addButton(ui->radioButton_Bad, 3);
+    compatibility->addButton(ui->radioButton_IntroMenu, 4);
+    compatibility->addButton(ui->radioButton_WontBoot, 5);
+    switch ((static_cast<CompatDBPage>(currentId()))) {
+    case CompatDBPage::Selection:
+        if (compatibility->checkedId() == -1) {
+            button(NextButton)->setEnabled(false);
+        }
+        break;
+    case CompatDBPage::Final:
+        LOG_DEBUG(Frontend, "Compatibility Rating: {}", compatibility->checkedId());
+        Core::Telemetry().AddField(Telemetry::FieldType::UserFeedback, "Compatibility",
+                                   compatibility->checkedId());
+        // older versions of QT don't support the "NoCancelButtonOnLastPage" option, this is a
+        // workaround
+        button(QWizard::CancelButton)->setVisible(false);
+        break;
+    default:
+        LOG_ERROR(Frontend, "Unexpected page: {}", currentId());
+    }
+}
+
+void CompatDB::EnableNext() {
+    button(NextButton)->setEnabled(true);
+}
diff --git a/src/yuzu/compatdb.h b/src/yuzu/compatdb.h
new file mode 100644
index 0000000000000000000000000000000000000000..0a0f27cca5d139a600ccb831fde29f1ed0699ddd
--- /dev/null
+++ b/src/yuzu/compatdb.h
@@ -0,0 +1,27 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QWizard>
+
+namespace Ui {
+class CompatDB;
+}
+
+class CompatDB : public QWizard {
+    Q_OBJECT
+
+public:
+    explicit CompatDB(QWidget* parent = nullptr);
+    ~CompatDB();
+
+private:
+    std::unique_ptr<Ui::CompatDB> ui;
+
+private slots:
+    void Submit();
+    void EnableNext();
+};
diff --git a/src/yuzu/compatdb.ui b/src/yuzu/compatdb.ui
new file mode 100644
index 0000000000000000000000000000000000000000..fed4021765299b82d7bd42b9e5e81dfa3eb28639
--- /dev/null
+++ b/src/yuzu/compatdb.ui
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CompatDB</class>
+ <widget class="QWizard" name="CompatDB">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>482</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>500</width>
+    <height>410</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Report Compatibility</string>
+  </property>
+  <property name="options">
+   <set>QWizard::DisabledBackButtonOnLastPage|QWizard::HelpButtonOnRight|QWizard::NoBackButtonOnStartPage</set>
+  </property>
+  <widget class="QWizardPage" name="wizard_Info">
+   <property name="title">
+    <string>Report Game Compatibility</string>
+   </property>
+   <attribute name="pageId">
+    <string notr="true">0</string>
+   </attribute>
+   <layout class="QVBoxLayout" name="verticalLayout">
+    <item>
+     <widget class="QLabel" name="lbl_Spiel">
+      <property name="text">
+       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:10pt;&quot;&gt;Should you choose to submit a test case to the &lt;/span&gt;&lt;a href=&quot;https://yuzu-emu.org/game/&quot;&gt;&lt;span style=&quot; font-size:10pt; text-decoration: underline; color:#0000ff;&quot;&gt;yuzu Compatibility List&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-size:10pt;&quot;&gt;, The following information will be collected and displayed on the site:&lt;/span&gt;&lt;/p&gt;&lt;ul style=&quot;margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;&quot;&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Hardware Information (CPU / GPU / Operating System)&lt;/li&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Which version of yuzu you are running&lt;/li&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;The connected yuzu account&lt;/li&gt;&lt;/ul&gt;&lt;/body&gt;&lt;/html&gt;</string>
+      </property>
+      <property name="wordWrap">
+       <bool>true</bool>
+      </property>
+      <property name="openExternalLinks">
+       <bool>true</bool>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <spacer name="verticalSpacer_2">
+      <property name="orientation">
+       <enum>Qt::Vertical</enum>
+      </property>
+      <property name="sizeHint" stdset="0">
+       <size>
+        <width>20</width>
+        <height>0</height>
+       </size>
+      </property>
+     </spacer>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QWizardPage" name="wizard_Report">
+   <property name="title">
+    <string>Report Game Compatibility</string>
+   </property>
+   <attribute name="pageId">
+    <string notr="true">1</string>
+   </attribute>
+   <layout class="QFormLayout" name="formLayout">
+    <item row="2" column="0">
+     <widget class="QRadioButton" name="radioButton_Perfect">
+      <property name="text">
+       <string>Perfect</string>
+      </property>
+     </widget>
+    </item>
+    <item row="2" column="1">
+     <widget class="QLabel" name="lbl_Perfect">
+      <property name="text">
+       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game functions flawlessly with no audio or graphical glitches.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+      </property>
+      <property name="wordWrap">
+       <bool>true</bool>
+      </property>
+     </widget>
+    </item>
+    <item row="4" column="0">
+     <widget class="QRadioButton" name="radioButton_Great">
+      <property name="text">
+       <string>Great </string>
+      </property>
+     </widget>
+    </item>
+    <item row="4" column="1">
+     <widget class="QLabel" name="lbl_Great">
+      <property name="text">
+       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game functions with minor graphical or audio glitches and is playable from start to finish. May require some workarounds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+      </property>
+      <property name="wordWrap">
+       <bool>true</bool>
+      </property>
+     </widget>
+    </item>
+    <item row="5" column="0">
+     <widget class="QRadioButton" name="radioButton_Okay">
+      <property name="text">
+       <string>Okay</string>
+      </property>
+     </widget>
+    </item>
+    <item row="5" column="1">
+     <widget class="QLabel" name="lbl_Okay">
+      <property name="text">
+       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game functions with major graphical or audio glitches, but game is playable from start to finish with workarounds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+      </property>
+      <property name="wordWrap">
+       <bool>true</bool>
+      </property>
+     </widget>
+    </item>
+    <item row="6" column="0">
+     <widget class="QRadioButton" name="radioButton_Bad">
+      <property name="text">
+       <string>Bad</string>
+      </property>
+     </widget>
+    </item>
+    <item row="6" column="1">
+     <widget class="QLabel" name="lbl_Bad">
+      <property name="text">
+       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches even with workarounds.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+      </property>
+      <property name="wordWrap">
+       <bool>true</bool>
+      </property>
+     </widget>
+    </item>
+    <item row="7" column="0">
+     <widget class="QRadioButton" name="radioButton_IntroMenu">
+      <property name="text">
+       <string>Intro/Menu</string>
+      </property>
+     </widget>
+    </item>
+    <item row="7" column="1">
+     <widget class="QLabel" name="lbl_IntroMenu">
+      <property name="text">
+       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+      </property>
+      <property name="wordWrap">
+       <bool>true</bool>
+      </property>
+     </widget>
+    </item>
+    <item row="8" column="0">
+     <widget class="QRadioButton" name="radioButton_WontBoot">
+      <property name="text">
+       <string>Won't Boot</string>
+      </property>
+      <property name="checkable">
+       <bool>true</bool>
+      </property>
+      <property name="checked">
+       <bool>false</bool>
+      </property>
+     </widget>
+    </item>
+    <item row="8" column="1">
+     <widget class="QLabel" name="lbl_WontBoot">
+      <property name="text">
+       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The game crashes when attempting to startup.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+      </property>
+     </widget>
+    </item>
+    <item row="0" column="0" colspan="2">
+     <widget class="QLabel" name="lbl_Independent">
+      <property name="font">
+       <font>
+        <pointsize>10</pointsize>
+       </font>
+      </property>
+      <property name="text">
+       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Independent of speed or performance, how well does this game play from start to finish on this version of yuzu?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+      </property>
+      <property name="wordWrap">
+       <bool>true</bool>
+      </property>
+     </widget>
+    </item>
+    <item row="1" column="0" colspan="2">
+     <spacer name="verticalSpacer">
+      <property name="orientation">
+       <enum>Qt::Vertical</enum>
+      </property>
+      <property name="sizeHint" stdset="0">
+       <size>
+        <width>20</width>
+        <height>0</height>
+       </size>
+      </property>
+     </spacer>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QWizardPage" name="wizard_ThankYou">
+   <property name="title">
+    <string>Thank you for your submission!</string>
+   </property>
+   <attribute name="pageId">
+    <string notr="true">2</string>
+   </attribute>
+  </widget>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index d229225b4bb96aaf2136b2886d1ccbbf06d2f277..650dd03c00dfa6b17032ccedc2309c5d63cb0dec 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -136,8 +136,18 @@ void Config::ReadValues() {
     Settings::values.gdbstub_port = qt_config->value("gdbstub_port", 24689).toInt();
     qt_config->endGroup();
 
+    qt_config->beginGroup("WebService");
+    Settings::values.enable_telemetry = qt_config->value("enable_telemetry", true).toBool();
+    Settings::values.web_api_url =
+        qt_config->value("web_api_url", "https://api.yuzu-emu.org").toString().toStdString();
+    Settings::values.yuzu_username = qt_config->value("yuzu_username").toString().toStdString();
+    Settings::values.yuzu_token = qt_config->value("yuzu_token").toString().toStdString();
+    qt_config->endGroup();
+
     qt_config->beginGroup("UI");
     UISettings::values.theme = qt_config->value("theme", UISettings::themes[0].second).toString();
+    UISettings::values.enable_discord_presence =
+        qt_config->value("enable_discord_presence", true).toBool();
 
     qt_config->beginGroup("UIGameList");
     UISettings::values.show_unknown = qt_config->value("show_unknown", true).toBool();
@@ -261,8 +271,16 @@ void Config::SaveValues() {
     qt_config->setValue("gdbstub_port", Settings::values.gdbstub_port);
     qt_config->endGroup();
 
+    qt_config->beginGroup("WebService");
+    qt_config->setValue("enable_telemetry", Settings::values.enable_telemetry);
+    qt_config->setValue("web_api_url", QString::fromStdString(Settings::values.web_api_url));
+    qt_config->setValue("yuzu_username", QString::fromStdString(Settings::values.yuzu_username));
+    qt_config->setValue("yuzu_token", QString::fromStdString(Settings::values.yuzu_token));
+    qt_config->endGroup();
+
     qt_config->beginGroup("UI");
     qt_config->setValue("theme", UISettings::values.theme);
+    qt_config->setValue("enable_discord_presence", UISettings::values.enable_discord_presence);
 
     qt_config->beginGroup("UIGameList");
     qt_config->setValue("show_unknown", UISettings::values.show_unknown);
diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui
index 20f1201342f1fd0d24bf8b7bf8087deebe9a7e11..9b297df283efbc28011fe7c3badbe7ba902b0e33 100644
--- a/src/yuzu/configuration/configure.ui
+++ b/src/yuzu/configuration/configure.ui
@@ -54,6 +54,11 @@
        <string>Debug</string>
       </attribute>
      </widget>
+      <widget class="ConfigureWeb" name="webTab">
+        <attribute name="title">
+          <string>Web</string>
+        </attribute>
+      </widget>
     </widget>
    </item>
    <item>
@@ -108,6 +113,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>
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp
index daa4cc0d96dafc3487f3caafd43e5b0604099e33..3905423e9e721d5a365765e723d0dbefc97ba5e7 100644
--- a/src/yuzu/configuration/configure_dialog.cpp
+++ b/src/yuzu/configuration/configure_dialog.cpp
@@ -27,5 +27,6 @@ void ConfigureDialog::applyConfiguration() {
     ui->graphicsTab->applyConfiguration();
     ui->audioTab->applyConfiguration();
     ui->debugTab->applyConfiguration();
+    ui->webTab->applyConfiguration();
     Settings::Apply();
 }
diff --git a/src/yuzu/configuration/configure_web.cpp b/src/yuzu/configuration/configure_web.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..cfca080147dc5d97e572b2ceb81c307df2935548
--- /dev/null
+++ b/src/yuzu/configuration/configure_web.cpp
@@ -0,0 +1,121 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QIcon>
+#include <QMessageBox>
+#include <QtConcurrent/QtConcurrentRun>
+#include "core/settings.h"
+#include "core/telemetry_session.h"
+#include "ui_configure_web.h"
+#include "yuzu/configuration/configure_web.h"
+#include "yuzu/ui_settings.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);
+    connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin);
+    connect(&verify_watcher, &QFutureWatcher<bool>::finished, this, &ConfigureWeb::OnLoginVerified);
+
+#ifndef USE_DISCORD_PRESENCE
+    ui->discord_group->setVisible(false);
+#endif
+    this->setConfiguration();
+}
+
+ConfigureWeb::~ConfigureWeb() {}
+
+void ConfigureWeb::setConfiguration() {
+    ui->web_credentials_disclaimer->setWordWrap(true);
+    ui->telemetry_learn_more->setOpenExternalLinks(true);
+    ui->telemetry_learn_more->setText(tr("<a "
+                                         "href='https://citra-emu.org/entry/"
+                                         "telemetry-and-why-thats-a-good-thing/'><span "
+                                         "style=\"text-decoration: underline; "
+                                         "color:#039be5;\">Learn more</span></a>"));
+
+    ui->web_signup_link->setOpenExternalLinks(true);
+    ui->web_signup_link->setText(
+        tr("<a href='https://services.citra-emu.org/'><span style=\"text-decoration: underline; "
+           "color:#039be5;\">Sign up</span></a>"));
+    ui->web_token_info_link->setOpenExternalLinks(true);
+    ui->web_token_info_link->setText(
+        tr("<a href='https://citra-emu.org/wiki/citra-web-service/'><span style=\"text-decoration: "
+           "underline; color:#039be5;\">What is my token?</span></a>"));
+
+    ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry);
+    ui->edit_username->setText(QString::fromStdString(Settings::values.yuzu_username));
+    ui->edit_token->setText(QString::fromStdString(Settings::values.yuzu_token));
+    // Connect after setting the values, to avoid calling OnLoginChanged now
+    connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged);
+    connect(ui->edit_username, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged);
+    ui->label_telemetry_id->setText(
+        tr("Telemetry ID: 0x%1").arg(QString::number(Core::GetTelemetryId(), 16).toUpper()));
+    user_verified = true;
+
+    ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence);
+}
+
+void ConfigureWeb::applyConfiguration() {
+    Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked();
+    UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked();
+    if (user_verified) {
+        Settings::values.yuzu_username = ui->edit_username->text().toStdString();
+        Settings::values.yuzu_token = ui->edit_token->text().toStdString();
+    } else {
+        QMessageBox::warning(this, tr("Username and token not verified"),
+                             tr("Username and token were not verified. The changes to your "
+                                "username and/or token have not been saved."));
+    }
+}
+
+void ConfigureWeb::RefreshTelemetryID() {
+    const u64 new_telemetry_id{Core::RegenerateTelemetryId()};
+    ui->label_telemetry_id->setText(
+        tr("Telemetry ID: 0x%1").arg(QString::number(new_telemetry_id, 16).toUpper()));
+}
+
+void ConfigureWeb::OnLoginChanged() {
+    if (ui->edit_username->text().isEmpty() && ui->edit_token->text().isEmpty()) {
+        user_verified = true;
+        ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16));
+        ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16));
+    } else {
+        user_verified = false;
+        ui->label_username_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16));
+        ui->label_token_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16));
+    }
+}
+
+void ConfigureWeb::VerifyLogin() {
+    ui->button_verify_login->setDisabled(true);
+    ui->button_verify_login->setText(tr("Verifying"));
+    verify_watcher.setFuture(
+        QtConcurrent::run([this, username = ui->edit_username->text().toStdString(),
+                           token = ui->edit_token->text().toStdString()]() {
+            return Core::VerifyLogin(username, token);
+        }));
+}
+
+void ConfigureWeb::OnLoginVerified() {
+    ui->button_verify_login->setEnabled(true);
+    ui->button_verify_login->setText(tr("Verify"));
+    if (verify_watcher.result()) {
+        user_verified = true;
+        ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16));
+        ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16));
+    } else {
+        ui->label_username_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16));
+        ui->label_token_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16));
+        QMessageBox::critical(
+            this, tr("Verification failed"),
+            tr("Verification failed. Check that you have entered your username and token "
+               "correctly, and that your internet connection is working."));
+    }
+}
+
+void ConfigureWeb::retranslateUi() {
+    ui->retranslateUi(this);
+}
diff --git a/src/yuzu/configuration/configure_web.h b/src/yuzu/configuration/configure_web.h
new file mode 100644
index 0000000000000000000000000000000000000000..7741ab95d7284a0476b2226a5146f9f135ddae2c
--- /dev/null
+++ b/src/yuzu/configuration/configure_web.h
@@ -0,0 +1,38 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QFutureWatcher>
+#include <QWidget>
+
+namespace Ui {
+class ConfigureWeb;
+}
+
+class ConfigureWeb : public QWidget {
+    Q_OBJECT
+
+public:
+    explicit ConfigureWeb(QWidget* parent = nullptr);
+    ~ConfigureWeb();
+
+    void applyConfiguration();
+    void retranslateUi();
+
+public slots:
+    void RefreshTelemetryID();
+    void OnLoginChanged();
+    void VerifyLogin();
+    void OnLoginVerified();
+
+private:
+    void setConfiguration();
+
+    bool user_verified = true;
+    QFutureWatcher<bool> verify_watcher;
+
+    std::unique_ptr<Ui::ConfigureWeb> ui;
+};
diff --git a/src/yuzu/configuration/configure_web.ui b/src/yuzu/configuration/configure_web.ui
new file mode 100644
index 0000000000000000000000000000000000000000..2f4b9dd73030c1642e31a22f8e1ecc466fbc9863
--- /dev/null
+++ b/src/yuzu/configuration/configure_web.ui
@@ -0,0 +1,206 @@
+<?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>926</width>
+    <height>561</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>yuzu Web Service</string>
+       </property>
+       <layout class="QVBoxLayout" name="verticalLayoutYuzuWebService">
+        <item>
+         <widget class="QLabel" name="web_credentials_disclaimer">
+          <property name="text">
+           <string>By providing your username and token, you agree to allow yuzu to collect additional usage data, which may include user identifying information.</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QGridLayout" name="gridLayoutYuzuUsername">
+          <item row="2" column="3">
+           <widget class="QPushButton" name="button_verify_login">
+            <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>Verify</string>
+            </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="0" column="1" colspan="3">
+           <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="4">
+           <widget class="QLabel" name="label_token_verified">
+           </widget>
+          </item>
+          <item row="0" column="0">
+           <widget class="QLabel" name="label_username">
+            <property name="text">
+             <string>Username: </string>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="4">
+           <widget class="QLabel" name="label_username_verified">
+           </widget>
+          </item>
+          <item row="1" column="1" colspan="3">
+           <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="1">
+           <widget class="QLabel" name="web_token_info_link">
+            <property name="text">
+             <string>What is my token?</string>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="2">
+           <spacer name="horizontalSpacer">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </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 yuzu 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>
+     <widget class="QGroupBox" name="discord_group">
+      <property name="title">
+       <string>Discord Presence</string>
+      </property>
+      <layout class="QVBoxLayout" name="verticalLayout_21">
+       <item>
+        <widget class="QCheckBox" name="toggle_discordrpc">
+         <property name="text">
+          <string>Show Current Game in your Discord Status</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+   </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>
diff --git a/src/yuzu/discord.h b/src/yuzu/discord.h
new file mode 100644
index 0000000000000000000000000000000000000000..a867cc4d6e36d02cb4373d8076fd94794dbce9b2
--- /dev/null
+++ b/src/yuzu/discord.h
@@ -0,0 +1,25 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+namespace DiscordRPC {
+
+class DiscordInterface {
+public:
+    virtual ~DiscordInterface() = default;
+
+    virtual void Pause() = 0;
+    virtual void Update() = 0;
+};
+
+class NullImpl : public DiscordInterface {
+public:
+    ~NullImpl() = default;
+
+    void Pause() override {}
+    void Update() override {}
+};
+
+} // namespace DiscordRPC
diff --git a/src/yuzu/discord_impl.cpp b/src/yuzu/discord_impl.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9d87a41ebba76ee5e868db2d354843bf022db4a0
--- /dev/null
+++ b/src/yuzu/discord_impl.cpp
@@ -0,0 +1,52 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <chrono>
+#include <string>
+#include <discord_rpc.h>
+#include "common/common_types.h"
+#include "core/core.h"
+#include "core/loader/loader.h"
+#include "yuzu/discord_impl.h"
+#include "yuzu/ui_settings.h"
+
+namespace DiscordRPC {
+
+DiscordImpl::DiscordImpl() {
+    DiscordEventHandlers handlers{};
+
+    // The number is the client ID for yuzu, it's used for images and the
+    // application name
+    Discord_Initialize("471872241299226636", &handlers, 1, nullptr);
+}
+
+DiscordImpl::~DiscordImpl() {
+    Discord_ClearPresence();
+    Discord_Shutdown();
+}
+
+void DiscordImpl::Pause() {
+    Discord_ClearPresence();
+}
+
+void DiscordImpl::Update() {
+    s64 start_time = std::chrono::duration_cast<std::chrono::seconds>(
+                         std::chrono::system_clock::now().time_since_epoch())
+                         .count();
+    std::string title;
+    if (Core::System::GetInstance().IsPoweredOn())
+        Core::System::GetInstance().GetAppLoader().ReadTitle(title);
+    DiscordRichPresence presence{};
+    presence.largeImageKey = "yuzu_logo";
+    presence.largeImageText = "yuzu is an emulator for the Nintendo Switch";
+    if (Core::System::GetInstance().IsPoweredOn()) {
+        presence.state = title.c_str();
+        presence.details = "Currently in game";
+    } else {
+        presence.details = "Not in game";
+    }
+    presence.startTimestamp = start_time;
+    Discord_UpdatePresence(&presence);
+}
+} // namespace DiscordRPC
diff --git a/src/yuzu/discord_impl.h b/src/yuzu/discord_impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..d71428c109f3b4477c50faf21929deb8e045eda0
--- /dev/null
+++ b/src/yuzu/discord_impl.h
@@ -0,0 +1,20 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "yuzu/discord.h"
+
+namespace DiscordRPC {
+
+class DiscordImpl : public DiscordInterface {
+public:
+    DiscordImpl();
+    ~DiscordImpl();
+
+    void Pause() override;
+    void Update() override;
+};
+
+} // namespace DiscordRPC
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 27015d02c53ff7afdab7f0cf9ef61ac942ecb6fc..2d6e0d4fc16ee7b29dfbfc8998280ecc1e034a30 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -35,6 +35,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
 #include <QtWidgets>
 #include <fmt/format.h>
 #include "common/common_paths.h"
+#include "common/detached_tasks.h"
 #include "common/file_util.h"
 #include "common/logging/backend.h"
 #include "common/logging/filter.h"
@@ -65,6 +66,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
 #include "video_core/debug_utils/debug_utils.h"
 #include "yuzu/about_dialog.h"
 #include "yuzu/bootmanager.h"
+#include "yuzu/compatdb.h"
 #include "yuzu/compatibility_list.h"
 #include "yuzu/configuration/config.h"
 #include "yuzu/configuration/configure_dialog.h"
@@ -73,12 +75,17 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
 #include "yuzu/debugger/graphics/graphics_surface.h"
 #include "yuzu/debugger/profiler.h"
 #include "yuzu/debugger/wait_tree.h"
+#include "yuzu/discord.h"
 #include "yuzu/game_list.h"
 #include "yuzu/game_list_p.h"
 #include "yuzu/hotkeys.h"
 #include "yuzu/main.h"
 #include "yuzu/ui_settings.h"
 
+#ifdef USE_DISCORD_PRESENCE
+#include "yuzu/discord_impl.h"
+#endif
+
 #ifdef QT_STATICPLUGIN
 Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
 #endif
@@ -102,23 +109,22 @@ enum class CalloutFlag : uint32_t {
     DRDDeprecation = 0x2,
 };
 
-static void ShowCalloutMessage(const QString& message, CalloutFlag flag) {
-    if (UISettings::values.callout_flags & static_cast<uint32_t>(flag)) {
+void GMainWindow::ShowTelemetryCallout() {
+    if (UISettings::values.callout_flags & static_cast<uint32_t>(CalloutFlag::Telemetry)) {
         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();
+    UISettings::values.callout_flags |= static_cast<uint32_t>(CalloutFlag::Telemetry);
+    static const QString telemetry_message =
+        tr("<a href='https://citra-emu.org/entry/telemetry-and-why-thats-a-good-thing/'>Anonymous "
+           "data is collected</a> to help improve yuzu. "
+           "<br/><br/>Would you like to share your usage data with us?");
+    if (QMessageBox::question(this, tr("Telemetry"), telemetry_message) != QMessageBox::Yes) {
+        Settings::values.enable_telemetry = false;
+        Settings::Apply();
+    }
 }
 
-void GMainWindow::ShowCallouts() {}
-
 const int GMainWindow::max_recent_files_item;
 
 static void InitializeLogging() {
@@ -145,6 +151,9 @@ GMainWindow::GMainWindow()
     default_theme_paths = QIcon::themeSearchPaths();
     UpdateUITheme();
 
+    SetDiscordEnabled(UISettings::values.enable_discord_presence);
+    discord_rpc->Update();
+
     InitializeWidgets();
     InitializeDebugWidgets();
     InitializeRecentFileMenuActions();
@@ -168,7 +177,7 @@ GMainWindow::GMainWindow()
     game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
 
     // Show one-time "callout" messages to the user
-    ShowCallouts();
+    ShowTelemetryCallout();
 
     QStringList args = QApplication::arguments();
     if (args.length() >= 2) {
@@ -183,6 +192,9 @@ GMainWindow::~GMainWindow() {
 }
 
 void GMainWindow::InitializeWidgets() {
+#ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING
+    ui.action_Report_Compatibility->setVisible(true);
+#endif
     render_window = new GRenderWindow(this, emu_thread.get());
     render_window->hide();
 
@@ -411,6 +423,8 @@ void GMainWindow::ConnectMenuEvents() {
     connect(ui.action_Start, &QAction::triggered, this, &GMainWindow::OnStartGame);
     connect(ui.action_Pause, &QAction::triggered, this, &GMainWindow::OnPauseGame);
     connect(ui.action_Stop, &QAction::triggered, this, &GMainWindow::OnStopGame);
+    connect(ui.action_Report_Compatibility, &QAction::triggered, this,
+            &GMainWindow::OnMenuReportCompatibility);
     connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); });
     connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
 
@@ -647,6 +661,7 @@ void GMainWindow::BootGame(const QString& filename) {
 }
 
 void GMainWindow::ShutdownGame() {
+    discord_rpc->Pause();
     emu_thread->RequestStop();
 
     emit EmulationStopping();
@@ -655,6 +670,8 @@ void GMainWindow::ShutdownGame() {
     emu_thread->wait();
     emu_thread = nullptr;
 
+    discord_rpc->Update();
+
     // The emulation is stopped, so closing the window or not does not matter anymore
     disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
 
@@ -664,6 +681,7 @@ void GMainWindow::ShutdownGame() {
     ui.action_Pause->setEnabled(false);
     ui.action_Stop->setEnabled(false);
     ui.action_Restart->setEnabled(false);
+    ui.action_Report_Compatibility->setEnabled(false);
     render_window->hide();
     game_list->show();
     game_list->setFilterFocus();
@@ -1147,6 +1165,9 @@ void GMainWindow::OnStartGame() {
     ui.action_Pause->setEnabled(true);
     ui.action_Stop->setEnabled(true);
     ui.action_Restart->setEnabled(true);
+    ui.action_Report_Compatibility->setEnabled(true);
+
+    discord_rpc->Update();
 }
 
 void GMainWindow::OnPauseGame() {
@@ -1161,6 +1182,20 @@ void GMainWindow::OnStopGame() {
     ShutdownGame();
 }
 
+void GMainWindow::OnMenuReportCompatibility() {
+    if (!Settings::values.yuzu_token.empty() && !Settings::values.yuzu_username.empty()) {
+        CompatDB compatdb{this};
+        compatdb.exec();
+    } else {
+        QMessageBox::critical(
+            this, tr("Missing yuzu Account"),
+            tr("In order to submit a game compatibility test case, you must link your yuzu "
+               "account.<br><br/>To link your yuzu account, go to Emulation &gt; Configuration "
+               "&gt; "
+               "Web."));
+    }
+}
+
 void GMainWindow::ToggleFullscreen() {
     if (!emulation_running) {
         return;
@@ -1224,11 +1259,14 @@ void GMainWindow::ToggleWindowMode() {
 void GMainWindow::OnConfigure() {
     ConfigureDialog configureDialog(this, hotkey_registry);
     auto old_theme = UISettings::values.theme;
+    const bool old_discord_presence = UISettings::values.enable_discord_presence;
     auto result = configureDialog.exec();
     if (result == QDialog::Accepted) {
         configureDialog.applyConfiguration();
         if (UISettings::values.theme != old_theme)
             UpdateUITheme();
+        if (UISettings::values.enable_discord_presence != old_discord_presence)
+            SetDiscordEnabled(UISettings::values.enable_discord_presence);
         game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
         config->Save();
     }
@@ -1443,11 +1481,25 @@ void GMainWindow::UpdateUITheme() {
     emit UpdateThemedIcons();
 }
 
+void GMainWindow::SetDiscordEnabled(bool state) {
+#ifdef USE_DISCORD_PRESENCE
+    if (state) {
+        discord_rpc = std::make_unique<DiscordRPC::DiscordImpl>();
+    } else {
+        discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
+    }
+#else
+    discord_rpc = std::make_unique<DiscordRPC::NullImpl>();
+#endif
+    discord_rpc->Update();
+}
+
 #ifdef main
 #undef main
 #endif
 
 int main(int argc, char* argv[]) {
+    Common::DetachedTasks detached_tasks;
     MicroProfileOnThreadCreate("Frontend");
     SCOPE_EXIT({ MicroProfileShutdown(); });
 
@@ -1465,5 +1517,7 @@ int main(int argc, char* argv[]) {
     GMainWindow main_window;
     // After settings have been loaded by GMainWindow, apply the filter
     main_window.show();
-    return app.exec();
+    int result = app.exec();
+    detached_tasks.WaitForAllTasks();
+    return result;
 }
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 8ee9242b1b176175a42769e8ac65a5bb1787fb71..fe0e9a50a2cfe92fb651c7f074069373cea12933 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -41,6 +41,10 @@ enum class EmulatedDirectoryTarget {
     SDMC,
 };
 
+namespace DiscordRPC {
+class DiscordInterface;
+}
+
 class GMainWindow : public QMainWindow {
     Q_OBJECT
 
@@ -61,6 +65,8 @@ public:
     GMainWindow();
     ~GMainWindow() override;
 
+    std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
+
 signals:
 
     /**
@@ -99,7 +105,8 @@ private:
     void BootGame(const QString& filename);
     void ShutdownGame();
 
-    void ShowCallouts();
+    void ShowTelemetryCallout();
+    void SetDiscordEnabled(bool state);
 
     /**
      * Stores the filename in the recently loaded files list.
@@ -135,6 +142,7 @@ private slots:
     void OnStartGame();
     void OnPauseGame();
     void OnStopGame();
+    void OnMenuReportCompatibility();
     /// Called whenever a user selects a game in the game list widget.
     void OnGameListLoadFile(QString game_path);
     void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target);
diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui
index 3879d481375cf8e4c08a794d42e7373836c4021a..cb1664b2190a34975c2f7b49695aa9728c51bc38 100644
--- a/src/yuzu/main.ui
+++ b/src/yuzu/main.ui
@@ -45,7 +45,7 @@
      <x>0</x>
      <y>0</y>
      <width>1081</width>
-     <height>19</height>
+     <height>21</height>
     </rect>
    </property>
    <widget class="QMenu" name="menu_File">
@@ -101,6 +101,8 @@
     <property name="title">
      <string>&amp;Help</string>
     </property>
+    <addaction name="action_Report_Compatibility"/>
+    <addaction name="separator"/>
     <addaction name="action_About"/>
    </widget>
    <addaction name="menu_File"/>
@@ -239,6 +241,18 @@
        <string>Restart</string>
      </property>
    </action>
+   <action name="action_Report_Compatibility">
+     <property name="enabled">
+       <bool>false</bool>
+     </property>
+     <property name="text">
+       <string>Report Compatibility</string>
+     </property>
+     <property name="visible">
+       <bool>false</bool>
+     </property>
+   </action>
   </widget>
  <resources/>
+ <connections/>
 </ui>
diff --git a/src/yuzu/ui_settings.h b/src/yuzu/ui_settings.h
index 051494bc57c4c2b2b57e2d84f20f6725a8137364..89d9140f372c44fcb06cfd91c5760af12215cee6 100644
--- a/src/yuzu/ui_settings.h
+++ b/src/yuzu/ui_settings.h
@@ -39,6 +39,9 @@ struct Values {
     bool confirm_before_closing;
     bool first_start;
 
+    // Discord RPC
+    bool enable_discord_presence;
+
     QString roms_path;
     QString symbols_path;
     QString gamedir;
diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp
index a478b0a568618c1669b0571636866efd631672be..9d934e2201975c09c2baeb629a57d94fa80af465 100644
--- a/src/yuzu_cmd/config.cpp
+++ b/src/yuzu_cmd/config.cpp
@@ -138,6 +138,14 @@ void Config::ReadValues() {
     Settings::values.use_gdbstub = sdl2_config->GetBoolean("Debugging", "use_gdbstub", false);
     Settings::values.gdbstub_port =
         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.web_api_url =
+        sdl2_config->Get("WebService", "web_api_url", "https://api.yuzu-emu.org");
+    Settings::values.yuzu_username = sdl2_config->Get("WebService", "yuzu_username", "");
+    Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", "");
 }
 
 void Config::Reload() {
diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h
index d35c441e92aec116a128e91ecfa108bee8e7911b..eaa64da396a40242af7bb7eeb5df5de14bcc80f7 100644
--- a/src/yuzu_cmd/default_ini.h
+++ b/src/yuzu_cmd/default_ini.h
@@ -202,10 +202,8 @@ gdbstub_port=24689
 # Whether or not to enable telemetry
 # 0: No, 1 (default): Yes
 enable_telemetry =
-# Endpoint URL for submitting telemetry data
-telemetry_endpoint_url =
-# Endpoint URL to verify the username and token
-verify_endpoint_url =
+# URL for Web API
+web_api_url = https://api.yuzu-emu.org
 # Username and token for yuzu Web Service
 # See https://services.citra-emu.org/ for more info
 yuzu_username =
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index b2559b71707a2d7e500675998a56c2462fb8c315..1d951ca3f1e0dfb926cdf87d788e161861e71287 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -10,6 +10,7 @@
 #include <fmt/ostream.h>
 
 #include "common/common_paths.h"
+#include "common/detached_tasks.h"
 #include "common/file_util.h"
 #include "common/logging/backend.h"
 #include "common/logging/filter.h"
@@ -78,6 +79,7 @@ static void InitializeLogging() {
 
 /// Application entry point
 int main(int argc, char** argv) {
+    Common::DetachedTasks detached_tasks;
     Config config;
 
     int option_index = 0;
@@ -213,5 +215,6 @@ int main(int argc, char** argv) {
         system.RunLoop();
     }
 
+    detached_tasks.WaitForAllTasks();
     return 0;
 }