Step 163: add telemetry and crash logging

This commit is contained in:
Bill
2026-02-09 19:56:55 -07:00
parent d5b11c4904
commit caa7c7fe42
8 changed files with 165 additions and 1 deletions

View File

@@ -510,3 +510,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
| 2026-02-10 | Codex | Step 160: Theme engine with JSON themes, ImGui styling, and syntax/editor color mapping. 4/4 tests pass. |
| 2026-02-10 | Codex | Step 161: Plugin API + loader with registry hooks for concepts/generators/grammars/annotations/panels/commands. 10/10 tests pass. |
| 2026-02-10 | Codex | Step 162: Help panel with Markdown sections + docs/help.md wired in UI. 4/4 tests pass. |
| 2026-02-10 | Codex | Step 163: Telemetry + crash logging (opt-in), settings toggle, and log writer. 3/3 tests pass. |

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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
View 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();
}
};

View File

@@ -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);

View 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;
}

View File

@@ -371,7 +371,7 @@ Final polish, documentation, and ecosystem features.
Content in Markdown, rendered in ImGui.
*New:* `HelpPanel.h`, markdown renderer
- [ ] **Step 163: Telemetry and crash reporting (opt-in)**
- [x] **Step 163: Telemetry and crash reporting (opt-in)**
Optional, anonymous usage telemetry to understand feature adoption.
Crash handler that captures stack traces on unhandled exceptions.
User must explicitly opt-in during first launch. Clear privacy policy.