Repository navigation
Telemetry System
The Telemetry System provides opt-in event recording for gameplay analytics and performance metrics. Consent is reversible and revocation clears the queue, but a producer/revocation race described below remains open; this page does not claim an absolute concurrent consent guarantee.
Source: SparkEngine/Source/Utils/Telemetry.h
| Class / Struct | Responsibility |
|---|---|
TelemetrySystem |
Singleton managing event recording, batching, auto-flush, and backend dispatch |
TelemetryConfig |
Configuration struct controlling enable state, consent, paths, batch sizes, and flush intervals |
TelemetryEvent |
A single telemetry data point with name, timestamp, properties, and session ID |
ITelemetryBackend |
Abstract interface for telemetry output destinations |
LocalFileTelemetryBackend |
Built-in backend that writes JSON files to disk |
struct TelemetryEvent
{
std::string name; // Event name (e.g. "level_complete")
uint64_t timestamp = 0; // Epoch milliseconds
std::unordered_map<std::string, std::string> properties; // Key-value metadata
std::string sessionId; // Session identifier
uint64_t sequence = 0; // In-memory flush ordering
};struct TelemetryConfig
{
bool enabled = false; // Master enable switch
bool consentGiven = false; // User has opted in
std::string localExportPath; // Directory for local JSON export
std::string httpEndpoint; // Remote endpoint URL (future use)
uint32_t batchSize = 50; // Events per flush batch
float flushIntervalSeconds = 30.0f; // Auto-flush interval
uint32_t maxQueueSize = 10000; // Maximum queued events before dropping
};#include "Utils/Telemetry.h"
auto& telemetry = Spark::TelemetrySystem::GetInstance();
Spark::TelemetryConfig cfg;
cfg.enabled = true;
cfg.consentGiven = true;
cfg.localExportPath = "telemetry/";
cfg.batchSize = 25;
cfg.flushIntervalSeconds = 60.0f;
telemetry.Initialize(cfg);
// Record a simple event
telemetry.RecordEvent("game_start");
// Record an event with properties
telemetry.RecordEvent("level_start", {
{"level", "3"},
{"difficulty", "hard"},
{"character", "warrior"}
});
// Record a timed event (e.g., level load duration)
telemetry.RecordTimedEvent("level_load", 2345.6f, {
{"level", "3"},
{"assets_loaded", "142"}
});
// Shutdown flushes remaining events
telemetry.Shutdown();// In your main loop:
void GameLoop(float dt)
{
// Auto-flushes when flushIntervalSeconds elapses
telemetry.Update(dt);
// Game logic that records events...
if (playerDied)
{
telemetry.RecordEvent("player_death", {
{"cause", "fall_damage"},
{"position_x", std::to_string(pos.x)},
{"position_y", std::to_string(pos.y)}
});
}
}// Force an immediate flush (e.g., before a loading screen)
telemetry.FlushEvents();const auto& config = telemetry.GetConfig();
uint32_t queued = telemetry.GetQueueSize();
bool hasConsent = telemetry.HasConsent();
Log::Info("Telemetry", "Enabled: {}, Consent: {}, Queued: {}/{}",
config.enabled, hasConsent, queued, config.maxQueueSize);| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
false |
Master enable switch; must be true for any recording |
consentGiven |
bool |
false |
User opt-in flag; must be true for any recording |
localExportPath |
std::string |
"" |
Directory for LocalFileTelemetryBackend JSON output |
httpEndpoint |
std::string |
"" |
Remote HTTP endpoint URL (reserved for future use) |
batchSize |
uint32_t |
50 |
Number of events per flush batch |
flushIntervalSeconds |
float |
30.0f |
Seconds between automatic flushes in Update()
|
maxQueueSize |
uint32_t |
10000 |
Maximum queued events; new events are silently dropped when full |
All three conditions must be true for RecordEvent() to accept an event:
-
m_initialized--Initialize()has been called -
m_config.enabled-- master switch is on -
m_config.consentGiven-- user has opted in
If any condition is observed false, events are silently dropped. This is the intended gate, but it does not close the concurrent producer/revocation race described below.
The telemetry system enforces strict privacy controls:
// User opts in via a settings menu
telemetry.SetConsent(true);
// User opts out -- immediately clears the event queue
telemetry.SetConsent(false);
// All queued events are gone; no further events will be recordedif (telemetry.HasConsent())
{
// Safe to show telemetry-related UI
}-
RecordEvent()andRecordTimedEvent()checkCanRecord()before constructing an event. -
SetConsent(false)is reversible and clears the queue while holding the queue mutex. - A producer can pass
CanRecord()just before revocation and enqueue after the clear because enqueue does not recheck consent under that mutex. Closing this race remains blocking OPS-100 work. - The in-memory queue has an event-count limit, but the current local backend has no durable retry, retention, spool-byte, or complete drop/failure accounting contract.
The local file backend writes JSON files to a specified directory. Each flush produces a timestamped file.
If TelemetryConfig::localExportPath is non-empty and no backend has been registered, Initialize() automatically creates a LocalFileTelemetryBackend:
Spark::TelemetryConfig cfg;
cfg.localExportPath = "telemetry/";
telemetry.Initialize(cfg);
// LocalFileTelemetryBackend is now activeEach flush writes a file like telemetry/telemetry_1712188800000.json:
[
{
"name": "level_start",
"timestamp": 1712188800000,
"sessionId": "session_1712188800000",
"properties": {
"level": "3",
"difficulty": "hard"
}
},
{
"name": "player_death",
"timestamp": 1712188830000,
"sessionId": "session_1712188800000",
"properties": {
"cause": "fall_damage"
}
}
]// If you have a direct reference to the backend:
auto* localBackend = dynamic_cast<Spark::LocalFileTelemetryBackend*>(backend.get());
if (localBackend)
{
uint32_t filesWritten = localBackend->GetFilesWritten();
}Implement ITelemetryBackend to send events to any destination:
class HttpTelemetryBackend final : public Spark::ITelemetryBackend
{
public:
explicit HttpTelemetryBackend(std::string endpoint)
: m_endpoint(std::move(endpoint)) {}
bool Send(const std::vector<Spark::TelemetryEvent>& events) override
{
// Serialize events to JSON
std::string payload = SerializeToJson(events);
// POST to the configured endpoint
auto response = HttpClient::Post(m_endpoint, payload);
return response.statusCode == 200;
}
std::string_view GetBackendName() const override
{
return "HTTP";
}
private:
std::string m_endpoint;
};auto& telemetry = Spark::TelemetrySystem::GetInstance();
// Register before or after Initialize() -- replaces any existing backend
telemetry.RegisterBackend(
std::make_unique<HttpTelemetryBackend>("https://analytics.example.com/v1/events")
);Note: RegisterBackend() replaces the current backend. Only one backend is active at a time. To fan out to multiple destinations, create a composite backend that delegates to multiple sub-backends.
| Method | Description |
|---|---|
Console_GetStatus() |
Returns initialization state, enabled/consent flags, queue size, total events recorded, backend name, and session ID |
Example output:
TelemetrySystem: initialized | Enabled: yes | Consent: yes | Queued: 12/10000 | Total recorded: 347 | Backend: LocalFile | Session: session_1712188800000
Call Update(dt) every frame for auto-flush behavior:
void Engine::Tick(float dt)
{
// ... other systems ...
telemetry.Update(dt);
}Record performance metrics as timed events:
auto start = std::chrono::steady_clock::now();
RenderFrame();
auto end = std::chrono::steady_clock::now();
float ms = std::chrono::duration<float, std::milli>(end - start).count();
telemetry.RecordTimedEvent("frame_render", ms, {
{"draw_calls", std::to_string(drawCallCount)},
{"triangles", std::to_string(triCount)}
});Wire consent to a user-facing toggle in the settings menu:
// In settings panel:
bool consent = telemetry.HasConsent();
if (ImGui::Checkbox("Allow anonymous usage data", &consent))
{
telemetry.SetConsent(consent);
SaveUserPreferences();
}Record packaging metrics for build analytics:
auto start = std::chrono::steady_clock::now();
auto result = packager.Package(cfg);
auto elapsed = std::chrono::duration<float, std::milli>(
std::chrono::steady_clock::now() - start).count();
telemetry.RecordTimedEvent("game_package", elapsed, {
{"success", result.success ? "true" : "false"},
{"assets", std::to_string(result.assetCount)},
{"size_mb", std::to_string(result.totalSizeMB)}
});Initialize() generates a session ID from the current epoch time (e.g., session_1712188800000). All events recorded in this session carry the same ID, enabling per-session grouping in analytics tools.
| Method | Signature | Description |
|---|---|---|
GetInstance |
static TelemetrySystem& GetInstance() |
Get the singleton instance |
Initialize |
void Initialize(const TelemetryConfig& config) |
Initialize with configuration; auto-creates LocalFile backend if path is set |
Shutdown |
void Shutdown() |
Flush remaining events, clear queue, release backend |
RecordEvent |
void RecordEvent(std::string_view name, const std::optional<std::unordered_map<std::string, std::string>>& properties = std::nullopt) |
Record a telemetry event with optional properties |
RecordTimedEvent |
void RecordTimedEvent(std::string_view name, float durationMs, const std::optional<std::unordered_map<std::string, std::string>>& properties = std::nullopt) |
Record a timed event with duration_ms property |
FlushEvents |
void FlushEvents() |
Flush all queued events to the backend immediately |
Update |
void Update(float dt) |
Per-frame update; auto-flushes when interval elapses |
SetConsent |
void SetConsent(bool consent) |
Set user consent; revoking clears the queue |
HasConsent |
bool HasConsent() const |
Check if user has given consent |
GetConfig |
const TelemetryConfig& GetConfig() const |
Get current configuration (read-only) |
GetQueueSize |
uint32_t GetQueueSize() const |
Number of events currently queued |
RegisterBackend |
void RegisterBackend(std::unique_ptr<ITelemetryBackend> backend) |
Register a custom backend (replaces existing) |
Console_GetStatus |
std::string Console_GetStatus() const |
Human-readable status string |
| Method | Signature | Description |
|---|---|---|
Send |
virtual bool Send(const std::vector<TelemetryEvent>& events) = 0 |
Send a batch of events; return true if accepted |
GetBackendName |
virtual std::string_view GetBackendName() const = 0 |
Human-readable backend name |
| Method | Signature | Description |
|---|---|---|
| Constructor | explicit LocalFileTelemetryBackend(std::string exportPath) |
Create with output directory path |
Send |
bool Send(const std::vector<TelemetryEvent>& events) override |
Write events to a timestamped JSON file |
GetBackendName |
std::string_view GetBackendName() const override |
Returns "LocalFile"
|
GetFilesWritten |
uint32_t GetFilesWritten() const |
Number of JSON files successfully written |
RecordEvent() supports multiple producers through the queue mutex and atomic sequence counter. Configuration, consent, flush, update, backend registration, and shutdown are not a general concurrently callable API; coordinate lifecycle operations on the owning thread. In particular, the consent check and enqueue are separate critical sections, which creates the revocation race above.
The singleton initialization is safe under C++11. LocalFileTelemetryBackend::Send() performs file I/O and should not be called concurrently on the same instance.
- Asset-Validation -- Record validation metrics as telemetry events
- Accessibility -- Track accessibility feature usage
- Platform-Input -- Record input analytics
- Game-Packaging -- Record packaging metrics
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers