Step 163: add telemetry and crash logging
This commit is contained in:
@@ -905,6 +905,10 @@ add_executable(step162_test tests/step162_test.cpp)
|
||||
target_include_directories(step162_test PRIVATE src)
|
||||
target_link_libraries(step162_test PRIVATE imgui::imgui)
|
||||
|
||||
add_executable(step163_test tests/step163_test.cpp)
|
||||
target_include_directories(step163_test PRIVATE src)
|
||||
target_link_libraries(step163_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "AgentMarketplace.h"
|
||||
#include "BuildSystem.h"
|
||||
#include "HelpPanel.h"
|
||||
#include "Telemetry.h"
|
||||
#include "DependencyPanel.h"
|
||||
#include "LibraryIndexer.h"
|
||||
#include "LibraryBrowserPanel.h"
|
||||
@@ -184,6 +185,7 @@ struct EditorState {
|
||||
AgentRegistry agentRegistry;
|
||||
AgentMarketplaceState agentMarketplace;
|
||||
HelpPanelState helpPanel;
|
||||
Telemetry telemetry;
|
||||
BuildSystem::Type buildType = BuildSystem::Type::None;
|
||||
std::vector<BuildError> buildErrors;
|
||||
std::string lastBuildOutput;
|
||||
@@ -818,6 +820,8 @@ struct EditorState {
|
||||
startEmacsDaemonFromSettings();
|
||||
initAgentServer();
|
||||
refreshBuildSystem();
|
||||
telemetry.initialize(workspaceRoot, settings.getTelemetryOptIn());
|
||||
telemetry.installCrashHandler();
|
||||
}
|
||||
|
||||
std::string orgTempExtension(const std::string& language) const {
|
||||
|
||||
@@ -34,6 +34,12 @@ public:
|
||||
void setTabSize(int size) { tabSize_ = size; }
|
||||
const std::string& getTheme() const { return theme_; }
|
||||
void setTheme(const std::string& theme) { theme_ = theme; }
|
||||
|
||||
bool getTelemetryOptIn() const { return telemetryOptIn_; }
|
||||
void setTelemetryOptIn(bool optIn) { telemetryOptIn_ = optIn; }
|
||||
|
||||
const std::string& getUpdateUrl() const { return updateUrl_; }
|
||||
void setUpdateUrl(const std::string& url) { updateUrl_ = url; }
|
||||
int getAutoSaveSeconds() const { return autoSaveSeconds_; }
|
||||
void setAutoSaveSeconds(int seconds) { autoSaveSeconds_ = seconds; }
|
||||
bool getShowMinimap() const { return showMinimap_; }
|
||||
@@ -54,6 +60,8 @@ public:
|
||||
fontSize_ = j.value("fontSize", fontSize_);
|
||||
tabSize_ = j.value("tabSize", tabSize_);
|
||||
theme_ = j.value("theme", theme_);
|
||||
telemetryOptIn_ = j.value("telemetryOptIn", telemetryOptIn_);
|
||||
updateUrl_ = j.value("updateUrl", updateUrl_);
|
||||
autoSaveSeconds_ = j.value("autoSaveSeconds", autoSaveSeconds_);
|
||||
showMinimap_ = j.value("showMinimap", showMinimap_);
|
||||
showLineNumbers_ = j.value("showLineNumbers", showLineNumbers_);
|
||||
@@ -84,6 +92,8 @@ public:
|
||||
j["fontSize"] = fontSize_;
|
||||
j["tabSize"] = tabSize_;
|
||||
j["theme"] = theme_;
|
||||
j["telemetryOptIn"] = telemetryOptIn_;
|
||||
j["updateUrl"] = updateUrl_;
|
||||
j["autoSaveSeconds"] = autoSaveSeconds_;
|
||||
j["showMinimap"] = showMinimap_;
|
||||
j["showLineNumbers"] = showLineNumbers_;
|
||||
@@ -151,6 +161,8 @@ private:
|
||||
int fontSize_ = 15;
|
||||
int tabSize_ = 4;
|
||||
std::string theme_ = "VSCode Dark";
|
||||
bool telemetryOptIn_ = false;
|
||||
std::string updateUrl_ = "https://example.com/whetstone/releases.json";
|
||||
int autoSaveSeconds_ = 0;
|
||||
bool showMinimap_ = false;
|
||||
bool showLineNumbers_ = true;
|
||||
|
||||
83
editor/src/Telemetry.h
Normal file
83
editor/src/Telemetry.h
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
// Step 163: Telemetry + crash reporting (opt-in)
|
||||
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
class Telemetry {
|
||||
public:
|
||||
void initialize(const std::string& root, bool optIn) {
|
||||
workspaceRoot_ = root;
|
||||
optIn_ = optIn;
|
||||
if (!workspaceRoot_.empty()) {
|
||||
telemetryPath_ = workspaceRoot_ + "/telemetry.log";
|
||||
crashPath_ = workspaceRoot_ + "/crash.log";
|
||||
}
|
||||
if (optIn_) {
|
||||
recordEvent("session_start", json::object());
|
||||
}
|
||||
}
|
||||
|
||||
void setOptIn(bool optIn) { optIn_ = optIn; }
|
||||
bool isOptedIn() const { return optIn_; }
|
||||
|
||||
void recordEvent(const std::string& name, const json& fields) {
|
||||
if (!optIn_) return;
|
||||
if (telemetryPath_.empty()) return;
|
||||
json payload = fields;
|
||||
payload["event"] = name;
|
||||
payload["timestamp"] = currentTimestamp();
|
||||
std::ofstream out(telemetryPath_, std::ios::app);
|
||||
if (out.is_open()) {
|
||||
out << payload.dump() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void installCrashHandler() {
|
||||
if (crashPath_.empty()) return;
|
||||
crashInstance_ = this;
|
||||
std::set_terminate(&Telemetry::onTerminate);
|
||||
}
|
||||
|
||||
const std::string& telemetryPath() const { return telemetryPath_; }
|
||||
const std::string& crashPath() const { return crashPath_; }
|
||||
|
||||
private:
|
||||
static std::string currentTimestamp() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t tt = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&tm, &tt);
|
||||
#else
|
||||
localtime_r(&tt, &tm);
|
||||
#endif
|
||||
char buf[32];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string workspaceRoot_;
|
||||
std::string telemetryPath_;
|
||||
std::string crashPath_;
|
||||
bool optIn_ = false;
|
||||
|
||||
inline static Telemetry* crashInstance_ = nullptr;
|
||||
|
||||
static void onTerminate() {
|
||||
if (crashInstance_ && !crashInstance_->crashPath_.empty()) {
|
||||
std::ofstream out(crashInstance_->crashPath_, std::ios::app);
|
||||
if (out.is_open()) {
|
||||
out << "[crash] terminate called at "
|
||||
<< currentTimestamp() << "\n";
|
||||
}
|
||||
}
|
||||
std::abort();
|
||||
}
|
||||
};
|
||||
@@ -866,6 +866,16 @@ int main(int, char**) {
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
bool telemetryOptIn = state.settings.getTelemetryOptIn();
|
||||
if (ImGui::Checkbox("Telemetry (opt-in)", &telemetryOptIn)) {
|
||||
state.settings.setTelemetryOptIn(telemetryOptIn);
|
||||
state.telemetry.setOptIn(telemetryOptIn);
|
||||
if (telemetryOptIn) {
|
||||
state.telemetry.recordEvent("telemetry_opt_in", json::object());
|
||||
}
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
int autoSave = state.settings.getAutoSaveSeconds();
|
||||
if (ImGui::InputInt("Auto-save (sec)", &autoSave)) {
|
||||
autoSave = std::max(0, autoSave);
|
||||
|
||||
50
editor/tests/step163_test.cpp
Normal file
50
editor/tests/step163_test.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
// Step 163 TDD Test: Telemetry opt-in logging
|
||||
#include "Telemetry.h"
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
|
||||
if (cond) {
|
||||
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string readFile(const std::string& path) {
|
||||
std::ifstream in(path);
|
||||
if (!in.is_open()) return "";
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
fs::path root = fs::temp_directory_path() / "whetstone_telemetry_test";
|
||||
fs::create_directories(root);
|
||||
|
||||
Telemetry telemetry;
|
||||
telemetry.initialize(root.string(), false);
|
||||
telemetry.recordEvent("nope", json::object());
|
||||
expect(!fs::exists(root / "telemetry.log"), "opt-out no log", passed, failed);
|
||||
|
||||
Telemetry telemetry2;
|
||||
telemetry2.initialize(root.string(), true);
|
||||
telemetry2.recordEvent("test_event", json::object());
|
||||
auto logPath = root / "telemetry.log";
|
||||
expect(fs::exists(logPath), "log written", passed, failed);
|
||||
std::string content = readFile(logPath.string());
|
||||
expect(content.find("test_event") != std::string::npos,
|
||||
"event recorded", passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 163 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user