added optional telemetry

main
Justin Barnett 2025-09-04 12:09:34 -04:00
parent 22e8016aee
commit e5039c8acc
11 changed files with 929 additions and 0 deletions

View File

@ -331,6 +331,18 @@ Help make MCP for Unity better!
--- ---
## 📊 Telemetry & Privacy
Unity MCP includes **privacy-focused, anonymous telemetry** to help us improve the product. We collect usage analytics and performance data, but **never** your code, project names, or personal information.
- **🔒 Anonymous**: Random UUIDs only, no personal data
- **🚫 Easy opt-out**: Set `DISABLE_TELEMETRY=true` environment variable
- **📖 Transparent**: See [TELEMETRY.md](TELEMETRY.md) for full details
Your privacy matters to us. All telemetry is optional and designed to respect your workflow.
---
## Troubleshooting ❓ ## Troubleshooting ❓
<details> <details>

178
TELEMETRY.md Normal file
View File

@ -0,0 +1,178 @@
# Unity MCP Telemetry
Unity MCP includes privacy-focused, anonymous telemetry to help us improve the product. This document explains what data is collected, how to opt out, and our privacy practices.
## 🔒 Privacy First
- **Anonymous**: We use randomly generated UUIDs - no personal information
- **Non-blocking**: Telemetry never interferes with your Unity workflow
- **Easy opt-out**: Simple environment variable or Unity Editor setting
- **Transparent**: All collected data types are documented here
## 📊 What We Collect
### Usage Analytics
- **Tool Usage**: Which MCP tools you use (manage_script, manage_scene, etc.)
- **Performance**: Execution times and success/failure rates
- **System Info**: Unity version, platform (Windows/Mac/Linux), MCP version
- **Milestones**: First-time usage events (first script creation, first tool use, etc.)
### Technical Diagnostics
- **Connection Events**: Bridge startup/connection success/failures
- **Error Reports**: Anonymized error messages (truncated to 200 chars)
- **Server Health**: Startup time, connection latency
### What We **DON'T** Collect
- ❌ Your code or script contents
- ❌ Project names, file names, or paths
- ❌ Personal information or identifiers
- ❌ Sensitive project data
- ❌ IP addresses (beyond what's needed for HTTP requests)
## 🚫 How to Opt Out
### Method 1: Environment Variable (Recommended)
Set any of these environment variables to `true`:
```bash
# Disable all telemetry
export DISABLE_TELEMETRY=true
# Unity MCP specific
export UNITY_MCP_DISABLE_TELEMETRY=true
# MCP protocol wide
export MCP_DISABLE_TELEMETRY=true
```
### Method 2: Unity Editor (Coming Soon)
In Unity Editor: `Window > MCP for Unity > Settings > Disable Telemetry`
### Method 3: Manual Config
Add to your MCP client config:
```json
{
"env": {
"DISABLE_TELEMETRY": "true"
}
}
```
## 🔧 Technical Implementation
### Architecture
- **Python Server**: Core telemetry collection and transmission
- **Unity Bridge**: Local event collection from Unity Editor
- **Anonymous UUIDs**: Generated per-installation for aggregate analytics
- **Thread-safe**: Non-blocking background transmission
- **Fail-safe**: Errors never interrupt your workflow
### Data Storage
Telemetry data is stored locally in:
- **Windows**: `%APPDATA%\UnityMCP\`
- **macOS**: `~/Library/Application Support/UnityMCP/`
- **Linux**: `~/.local/share/UnityMCP/`
Files created:
- `customer_uuid.txt`: Anonymous identifier
- `milestones.json`: One-time events tracker
### Data Transmission
- **Endpoint**: `https://telemetry.coplay.dev/unity-mcp/anonymous`
- **Method**: HTTPS POST with JSON payload
- **Retry**: Background thread with graceful failure
- **Timeout**: 10 second timeout, no retries on failure
## 📈 How We Use This Data
### Product Improvement
- **Feature Usage**: Understand which tools are most/least used
- **Performance**: Identify slow operations to optimize
- **Reliability**: Track error rates and connection issues
- **Compatibility**: Ensure Unity version compatibility
### Development Priorities
- **Roadmap**: Focus development on most-used features
- **Bug Fixes**: Prioritize fixes based on error frequency
- **Platform Support**: Allocate resources based on platform usage
- **Documentation**: Improve docs for commonly problematic areas
### What We Don't Do
- ❌ Sell data to third parties
- ❌ Use data for advertising/marketing
- ❌ Track individual developers
- ❌ Store sensitive project information
## 🛠️ For Developers
### Testing Telemetry
```bash
cd UnityMcpBridge/UnityMcpServer~/src
python test_telemetry.py
```
### Custom Telemetry Events
```python
from telemetry import record_telemetry, RecordType
record_telemetry(RecordType.USAGE, {
"custom_event": "my_feature_used",
"metadata": "optional_data"
})
```
### Telemetry Status Check
```python
from telemetry import is_telemetry_enabled
if is_telemetry_enabled():
print("Telemetry is active")
else:
print("Telemetry is disabled")
```
## 📋 Data Retention Policy
- **Aggregated Data**: Retained indefinitely for product insights
- **Raw Events**: Automatically purged after 90 days
- **Personal Data**: None collected, so none to purge
- **Opt-out**: Immediate - no data sent after opting out
## 🤝 Contact & Transparency
- **Questions**: [Discord Community](https://discord.gg/y4p8KfzrN4)
- **Issues**: [GitHub Issues](https://github.com/CoplayDev/unity-mcp/issues)
- **Privacy Concerns**: Create a GitHub issue with "Privacy" label
- **Source Code**: All telemetry code is open source in this repository
## 📊 Example Telemetry Event
Here's what a typical telemetry event looks like:
```json
{
"record": "tool_execution",
"timestamp": 1704067200,
"customer_uuid": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "abc123-def456-ghi789",
"version": "3.0.2",
"platform": "posix",
"data": {
"tool_name": "manage_script",
"success": true,
"duration_ms": 42.5
}
}
```
Notice:
- ✅ Anonymous UUID (randomly generated)
- ✅ Tool performance metrics
- ✅ Success/failure tracking
- ❌ No code content
- ❌ No project information
- ❌ No personal data
---
*Unity MCP Telemetry is designed to respect your privacy while helping us build a better tool. Thank you for helping improve Unity MCP!*

View File

@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Unity Bridge telemetry helper for collecting usage analytics
/// Following privacy-first approach with easy opt-out mechanisms
/// </summary>
public static class TelemetryHelper
{
private const string TELEMETRY_DISABLED_KEY = "MCPForUnity.TelemetryDisabled";
private const string CUSTOMER_UUID_KEY = "MCPForUnity.CustomerUUID";
/// <summary>
/// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs)
/// </summary>
public static bool IsEnabled
{
get
{
// Check environment variables first
var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY");
if (!string.IsNullOrEmpty(envDisable) &&
(envDisable.ToLower() == "true" || envDisable == "1"))
{
return false;
}
var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY");
if (!string.IsNullOrEmpty(unityMcpDisable) &&
(unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1"))
{
return false;
}
// Check EditorPrefs
return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false);
}
}
/// <summary>
/// Get or generate customer UUID for anonymous tracking
/// </summary>
public static string GetCustomerUUID()
{
var uuid = UnityEditor.EditorPrefs.GetString(CUSTOMER_UUID_KEY, "");
if (string.IsNullOrEmpty(uuid))
{
uuid = System.Guid.NewGuid().ToString();
UnityEditor.EditorPrefs.SetString(CUSTOMER_UUID_KEY, uuid);
}
return uuid;
}
/// <summary>
/// Disable telemetry (stored in EditorPrefs)
/// </summary>
public static void DisableTelemetry()
{
UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true);
}
/// <summary>
/// Enable telemetry (stored in EditorPrefs)
/// </summary>
public static void EnableTelemetry()
{
UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false);
}
/// <summary>
/// Send telemetry data to Python server for processing
/// This is a lightweight bridge - the actual telemetry logic is in Python
/// </summary>
public static void RecordEvent(string eventType, Dictionary<string, object> data = null)
{
if (!IsEnabled)
return;
try
{
var telemetryData = new Dictionary<string, object>
{
["event_type"] = eventType,
["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["customer_uuid"] = GetCustomerUUID(),
["unity_version"] = Application.unityVersion,
["platform"] = Application.platform.ToString(),
["source"] = "unity_bridge"
};
if (data != null)
{
telemetryData["data"] = data;
}
// Send to Python server via existing bridge communication
// The Python server will handle actual telemetry transmission
SendTelemetryToPythonServer(telemetryData);
}
catch (Exception e)
{
// Never let telemetry errors interfere with functionality
if (IsDebugEnabled())
{
Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}");
}
}
}
/// <summary>
/// Record bridge startup event
/// </summary>
public static void RecordBridgeStartup()
{
RecordEvent("bridge_startup", new Dictionary<string, object>
{
["bridge_version"] = "3.0.2",
["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode
});
}
/// <summary>
/// Record bridge connection event
/// </summary>
public static void RecordBridgeConnection(bool success, string error = null)
{
var data = new Dictionary<string, object>
{
["success"] = success
};
if (!string.IsNullOrEmpty(error))
{
data["error"] = error.Substring(0, Math.Min(200, error.Length));
}
RecordEvent("bridge_connection", data);
}
/// <summary>
/// Record tool execution from Unity side
/// </summary>
public static void RecordToolExecution(string toolName, bool success, float durationMs, string error = null)
{
var data = new Dictionary<string, object>
{
["tool_name"] = toolName,
["success"] = success,
["duration_ms"] = Math.Round(durationMs, 2)
};
if (!string.IsNullOrEmpty(error))
{
data["error"] = error.Substring(0, Math.Min(200, error.Length));
}
RecordEvent("tool_execution_unity", data);
}
private static void SendTelemetryToPythonServer(Dictionary<string, object> telemetryData)
{
// This would integrate with the existing bridge communication system
// For now, we'll just log it when debug is enabled
if (IsDebugEnabled())
{
Debug.Log($"<b><color=#2EA3FF>MCP-TELEMETRY</color></b>: {telemetryData["event_type"]}");
}
// TODO: Integrate with MCPForUnityBridge command system
// We would send this as a special telemetry command to the Python server
}
private static bool IsDebugEnabled()
{
try
{
return UnityEditor.EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
}
catch
{
return false;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b8f3c2d1e7a94f6c8a9b5e3d2c1a0f9e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -67,10 +67,16 @@ namespace MCPForUnity.Editor
currentUnityPort = PortManager.GetPortWithFallback(); currentUnityPort = PortManager.GetPortWithFallback();
Start(); Start();
isAutoConnectMode = true; isAutoConnectMode = true;
// Record telemetry for bridge startup
TelemetryHelper.RecordBridgeStartup();
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogError($"Auto-connect failed: {ex.Message}"); Debug.LogError($"Auto-connect failed: {ex.Message}");
// Record telemetry for connection failure
TelemetryHelper.RecordBridgeConnection(false, ex.Message);
throw; throw;
} }
} }

View File

@ -31,5 +31,9 @@ class ServerConfig:
# 40 × 250ms ≈ 10s default window # 40 × 250ms ≈ 10s default window
reload_max_retries: int = 40 reload_max_retries: int = 40
# Telemetry settings
telemetry_enabled: bool = True
telemetry_endpoint: str = "https://telemetry.coplay.dev/unity-mcp/anonymous"
# Create a global config instance # Create a global config instance
config = ServerConfig() config = ServerConfig()

View File

@ -6,6 +6,8 @@ from typing import AsyncIterator, Dict, Any, List
from config import config from config import config
from tools import register_all_tools from tools import register_all_tools
from unity_connection import get_unity_connection, UnityConnection from unity_connection import get_unity_connection, UnityConnection
from telemetry import record_telemetry, record_milestone, RecordType, MilestoneType
import time
# Configure logging using settings from config # Configure logging using settings from config
logging.basicConfig( logging.basicConfig(
@ -22,12 +24,38 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
"""Handle server startup and shutdown.""" """Handle server startup and shutdown."""
global _unity_connection global _unity_connection
logger.info("MCP for Unity Server starting up") logger.info("MCP for Unity Server starting up")
# Record server startup telemetry
start_time = time.time()
record_telemetry(RecordType.STARTUP, {
"server_version": "3.0.2",
"startup_time": start_time
})
# Record first startup milestone
record_milestone(MilestoneType.FIRST_STARTUP)
try: try:
_unity_connection = get_unity_connection() _unity_connection = get_unity_connection()
logger.info("Connected to Unity on startup") logger.info("Connected to Unity on startup")
# Record successful Unity connection
record_telemetry(RecordType.UNITY_CONNECTION, {
"status": "connected",
"connection_time_ms": (time.time() - start_time) * 1000
})
except Exception as e: except Exception as e:
logger.warning(f"Could not connect to Unity on startup: {str(e)}") logger.warning(f"Could not connect to Unity on startup: {str(e)}")
_unity_connection = None _unity_connection = None
# Record connection failure
record_telemetry(RecordType.UNITY_CONNECTION, {
"status": "failed",
"error": str(e)[:200],
"connection_time_ms": (time.time() - start_time) * 1000
})
try: try:
# Yield the connection object so it can be attached to the context # Yield the connection object so it can be attached to the context
# The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge) # The key 'bridge' matches how tools like read_console expect to access it (ctx.bridge)

View File

@ -0,0 +1,289 @@
"""
Privacy-focused, anonymous telemetry system for Unity MCP
Inspired by Onyx's telemetry implementation with Unity-specific adaptations
"""
import uuid
import threading
import contextvars
import json
import time
import os
import logging
from enum import Enum
from dataclasses import dataclass, asdict
from typing import Optional, Dict, Any, List
from pathlib import Path
try:
import httpx
HAS_HTTPX = True
except ImportError:
httpx = None # type: ignore
HAS_HTTPX = False
logger = logging.getLogger("unity-mcp-telemetry")
class RecordType(str, Enum):
"""Types of telemetry records we collect"""
VERSION = "version"
STARTUP = "startup"
USAGE = "usage"
LATENCY = "latency"
FAILURE = "failure"
TOOL_EXECUTION = "tool_execution"
UNITY_CONNECTION = "unity_connection"
CLIENT_CONNECTION = "client_connection"
class MilestoneType(str, Enum):
"""Major user journey milestones"""
FIRST_STARTUP = "first_startup"
FIRST_TOOL_USAGE = "first_tool_usage"
FIRST_SCRIPT_CREATION = "first_script_creation"
FIRST_SCENE_MODIFICATION = "first_scene_modification"
MULTIPLE_SESSIONS = "multiple_sessions"
DAILY_ACTIVE_USER = "daily_active_user"
WEEKLY_ACTIVE_USER = "weekly_active_user"
@dataclass
class TelemetryRecord:
"""Structure for telemetry data"""
record_type: RecordType
timestamp: float
customer_uuid: str
session_id: str
data: Dict[str, Any]
milestone: Optional[MilestoneType] = None
class TelemetryConfig:
"""Telemetry configuration"""
def __init__(self):
# Check environment variables for opt-out
self.enabled = not self._is_disabled()
# Telemetry endpoint - can be configured via environment
self.endpoint = os.environ.get(
"UNITY_MCP_TELEMETRY_ENDPOINT",
"https://telemetry.coplay.dev/unity-mcp/anonymous"
)
# Local storage for UUID and milestones
self.data_dir = self._get_data_directory()
self.uuid_file = self.data_dir / "customer_uuid.txt"
self.milestones_file = self.data_dir / "milestones.json"
# Request timeout
self.timeout = 10.0
# Session tracking
self.session_id = str(uuid.uuid4())
def _is_disabled(self) -> bool:
"""Check if telemetry is disabled via environment variables"""
disable_vars = [
"DISABLE_TELEMETRY",
"UNITY_MCP_DISABLE_TELEMETRY",
"MCP_DISABLE_TELEMETRY"
]
for var in disable_vars:
if os.environ.get(var, "").lower() in ("true", "1", "yes", "on"):
return True
return False
def _get_data_directory(self) -> Path:
"""Get directory for storing telemetry data"""
if os.name == 'nt': # Windows
base_dir = Path(os.environ.get('APPDATA', Path.home() / 'AppData' / 'Roaming'))
elif os.name == 'posix': # macOS/Linux
if 'darwin' in os.uname().sysname.lower(): # macOS
base_dir = Path.home() / 'Library' / 'Application Support'
else: # Linux
base_dir = Path(os.environ.get('XDG_DATA_HOME', Path.home() / '.local' / 'share'))
else:
base_dir = Path.home() / '.unity-mcp'
data_dir = base_dir / 'UnityMCP'
data_dir.mkdir(parents=True, exist_ok=True)
return data_dir
class TelemetryCollector:
"""Main telemetry collection class"""
def __init__(self):
self.config = TelemetryConfig()
self._customer_uuid: Optional[str] = None
self._milestones: Dict[str, Dict[str, Any]] = {}
self._load_persistent_data()
def _load_persistent_data(self):
"""Load UUID and milestones from disk"""
try:
# Load customer UUID
if self.config.uuid_file.exists():
self._customer_uuid = self.config.uuid_file.read_text().strip()
else:
self._customer_uuid = str(uuid.uuid4())
self.config.uuid_file.write_text(self._customer_uuid)
# Load milestones
if self.config.milestones_file.exists():
self._milestones = json.loads(self.config.milestones_file.read_text())
except Exception as e:
logger.warning(f"Failed to load telemetry data: {e}")
self._customer_uuid = str(uuid.uuid4())
self._milestones = {}
def _save_milestones(self):
"""Save milestones to disk"""
try:
self.config.milestones_file.write_text(json.dumps(self._milestones, indent=2))
except Exception as e:
logger.warning(f"Failed to save milestones: {e}")
def record_milestone(self, milestone: MilestoneType, data: Optional[Dict[str, Any]] = None) -> bool:
"""Record a milestone event, returns True if this is the first occurrence"""
if not self.config.enabled:
return False
milestone_key = milestone.value
if milestone_key in self._milestones:
return False # Already recorded
milestone_data = {
"timestamp": time.time(),
"data": data or {}
}
self._milestones[milestone_key] = milestone_data
self._save_milestones()
# Also send as telemetry record
self.record(
record_type=RecordType.USAGE,
data={"milestone": milestone_key, **(data or {})},
milestone=milestone
)
return True
def record(self,
record_type: RecordType,
data: Dict[str, Any],
milestone: Optional[MilestoneType] = None):
"""Record a telemetry event (async, non-blocking)"""
if not self.config.enabled:
return
if not HAS_HTTPX:
logger.debug("Telemetry disabled: httpx not available")
return
record = TelemetryRecord(
record_type=record_type,
timestamp=time.time(),
customer_uuid=self._customer_uuid or "unknown",
session_id=self.config.session_id,
data=data,
milestone=milestone
)
# Send in background thread to avoid blocking
current_context = contextvars.copy_context()
thread = threading.Thread(
target=lambda: current_context.run(self._send_telemetry, record),
daemon=True
)
thread.start()
def _send_telemetry(self, record: TelemetryRecord):
"""Send telemetry data to endpoint"""
try:
payload = {
"record": record.record_type.value,
"timestamp": record.timestamp,
"customer_uuid": record.customer_uuid,
"session_id": record.session_id,
"data": record.data,
"version": "3.0.2", # Unity MCP version
"platform": os.name
}
if record.milestone:
payload["milestone"] = record.milestone.value
if not httpx:
return
with httpx.Client(timeout=self.config.timeout) as client:
response = client.post(self.config.endpoint, json=payload)
if response.status_code == 200:
logger.debug(f"Telemetry sent: {record.record_type}")
else:
logger.debug(f"Telemetry failed: HTTP {response.status_code}")
except Exception as e:
# Never let telemetry errors interfere with app functionality
logger.debug(f"Telemetry send failed: {e}")
# Global telemetry instance
_telemetry_collector: Optional[TelemetryCollector] = None
def get_telemetry() -> TelemetryCollector:
"""Get the global telemetry collector instance"""
global _telemetry_collector
if _telemetry_collector is None:
_telemetry_collector = TelemetryCollector()
return _telemetry_collector
def record_telemetry(record_type: RecordType,
data: Dict[str, Any],
milestone: Optional[MilestoneType] = None):
"""Convenience function to record telemetry"""
get_telemetry().record(record_type, data, milestone)
def record_milestone(milestone: MilestoneType, data: Optional[Dict[str, Any]] = None) -> bool:
"""Convenience function to record a milestone"""
return get_telemetry().record_milestone(milestone, data)
def record_tool_usage(tool_name: str, success: bool, duration_ms: float, error: Optional[str] = None):
"""Record tool usage telemetry"""
data = {
"tool_name": tool_name,
"success": success,
"duration_ms": round(duration_ms, 2)
}
if error:
data["error"] = str(error)[:200] # Limit error message length
record_telemetry(RecordType.TOOL_EXECUTION, data)
def record_latency(operation: str, duration_ms: float, metadata: Optional[Dict[str, Any]] = None):
"""Record latency telemetry"""
data = {
"operation": operation,
"duration_ms": round(duration_ms, 2)
}
if metadata:
data.update(metadata)
record_telemetry(RecordType.LATENCY, data)
def record_failure(component: str, error: str, metadata: Optional[Dict[str, Any]] = None):
"""Record failure telemetry"""
data = {
"component": component,
"error": str(error)[:500] # Limit error message length
}
if metadata:
data.update(metadata)
record_telemetry(RecordType.FAILURE, data)
def is_telemetry_enabled() -> bool:
"""Check if telemetry is enabled"""
return get_telemetry().config.enabled

View File

@ -0,0 +1,42 @@
"""
Telemetry decorator for Unity MCP tools
"""
import functools
import time
from typing import Callable, Any
from telemetry import record_tool_usage, record_milestone, MilestoneType
def telemetry_tool(tool_name: str):
"""Decorator to add telemetry tracking to MCP tools"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
start_time = time.time()
success = False
error = None
try:
result = func(*args, **kwargs)
success = True
# Record tool-specific milestones
if tool_name == "manage_script" and kwargs.get("action") == "create":
record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
elif tool_name.startswith("manage_scene"):
record_milestone(MilestoneType.FIRST_SCENE_MODIFICATION)
# Record general first tool usage
record_milestone(MilestoneType.FIRST_TOOL_USAGE)
return result
except Exception as e:
error = str(e)
raise
finally:
duration_ms = (time.time() - start_time) * 1000
record_tool_usage(tool_name, success, duration_ms, error)
return wrapper
return decorator

View File

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""
Test script for Unity MCP Telemetry System
Run this to verify telemetry is working correctly
"""
import os
import time
import sys
from pathlib import Path
# Add src to Python path for imports
sys.path.insert(0, str(Path(__file__).parent))
def test_telemetry_basic():
"""Test basic telemetry functionality"""
print("🧪 Testing Unity MCP Telemetry System...")
try:
from telemetry import (
get_telemetry, record_telemetry, record_milestone,
RecordType, MilestoneType, is_telemetry_enabled
)
print("✅ Telemetry module imported successfully")
except ImportError as e:
print(f"❌ Failed to import telemetry module: {e}")
return False
# Test telemetry enabled status
print(f"📊 Telemetry enabled: {is_telemetry_enabled()}")
# Test basic record
try:
record_telemetry(RecordType.VERSION, {
"version": "3.0.2",
"test_run": True
})
print("✅ Basic telemetry record sent")
except Exception as e:
print(f"❌ Failed to send basic telemetry: {e}")
return False
# Test milestone recording
try:
is_first = record_milestone(MilestoneType.FIRST_STARTUP, {
"test_mode": True
})
print(f"✅ Milestone recorded (first time: {is_first})")
except Exception as e:
print(f"❌ Failed to record milestone: {e}")
return False
# Test telemetry collector
try:
collector = get_telemetry()
print(f"✅ Telemetry collector initialized (UUID: {collector._customer_uuid[:8]}...)")
except Exception as e:
print(f"❌ Failed to get telemetry collector: {e}")
return False
return True
def test_telemetry_disabled():
"""Test telemetry with disabled state"""
print("\n🚫 Testing telemetry disabled state...")
# Set environment variable to disable telemetry
os.environ["DISABLE_TELEMETRY"] = "true"
# Re-import to get fresh config
import importlib
import telemetry
importlib.reload(telemetry)
from telemetry import is_telemetry_enabled, record_telemetry, RecordType
print(f"📊 Telemetry enabled (should be False): {is_telemetry_enabled()}")
if not is_telemetry_enabled():
print("✅ Telemetry correctly disabled via environment variable")
# Test that records are ignored when disabled
record_telemetry(RecordType.USAGE, {"test": "should_be_ignored"})
print("✅ Telemetry record ignored when disabled")
return True
else:
print("❌ Telemetry not disabled by environment variable")
return False
def test_data_storage():
"""Test data storage functionality"""
print("\n💾 Testing data storage...")
try:
from telemetry import get_telemetry
collector = get_telemetry()
data_dir = collector.config.data_dir
print(f"📁 Data directory: {data_dir}")
print(f"🏷️ UUID file: {collector.config.uuid_file}")
print(f"🎯 Milestones file: {collector.config.milestones_file}")
# Check if files exist
if collector.config.uuid_file.exists():
print("✅ UUID file exists")
else:
print(" UUID file will be created on first use")
if collector.config.milestones_file.exists():
print("✅ Milestones file exists")
else:
print(" Milestones file will be created on first milestone")
return True
except Exception as e:
print(f"❌ Data storage test failed: {e}")
return False
def main():
"""Run all telemetry tests"""
print("🚀 Unity MCP Telemetry Test Suite")
print("=" * 50)
tests = [
test_telemetry_basic,
test_data_storage,
test_telemetry_disabled,
]
passed = 0
failed = 0
for test in tests:
try:
if test():
passed += 1
print("✅ PASSED\n")
else:
failed += 1
print("❌ FAILED\n")
except Exception as e:
failed += 1
print(f"❌ FAILED with exception: {e}\n")
print("=" * 50)
print(f"📊 Test Results: {passed} passed, {failed} failed")
if failed == 0:
print("🎉 All telemetry tests passed!")
return True
else:
print(f"⚠️ {failed} test(s) failed")
return False
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

View File

@ -5,11 +5,22 @@ from config import config
import time import time
import os import os
import base64 import base64
try:
from telemetry_decorator import telemetry_tool
from telemetry import record_milestone, MilestoneType
HAS_TELEMETRY = True
except ImportError:
HAS_TELEMETRY = False
def telemetry_tool(tool_name: str):
def decorator(func):
return func
return decorator
def register_manage_script_tools(mcp: FastMCP): def register_manage_script_tools(mcp: FastMCP):
"""Register all script management tools with the MCP server.""" """Register all script management tools with the MCP server."""
@mcp.tool() @mcp.tool()
@telemetry_tool("manage_script")
def manage_script( def manage_script(
ctx: Context, ctx: Context,
action: str, action: str,