Skip to content

Commit f6a5568

Browse files
committed
telemetry: prefer config with env override; validate scheme; robust load\n\n- TelemetryConfig reads config.telemetry_enabled/endpoint, env can override\n- Validate endpoint scheme; revalidate on send\n- Split UUID/milestones load error handling\n- Add tests for config precedence, scheme validation, UUID preservation\n- validate_script: optional include_diagnostics with documented behavior
1 parent 7f0527f commit f6a5568

File tree

3 files changed

+105
-17
lines changed

3 files changed

+105
-17
lines changed

UnityMcpBridge/UnityMcpServer~/src/telemetry.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from dataclasses import dataclass, asdict
1818
from typing import Optional, Dict, Any, List
1919
from pathlib import Path
20+
import importlib
2021

2122
try:
2223
import httpx
@@ -61,11 +62,29 @@ class TelemetryRecord:
6162
class TelemetryConfig:
6263
"""Telemetry configuration"""
6364
def __init__(self):
64-
# Check environment variables for opt-out
65-
self.enabled = not self._is_disabled()
65+
# Prefer config file, then allow env overrides
66+
server_config = None
67+
for modname in (
68+
"UnityMcpBridge.UnityMcpServer~.src.config",
69+
"UnityMcpBridge.UnityMcpServer.src.config",
70+
"src.config",
71+
"config",
72+
):
73+
try:
74+
mod = importlib.import_module(modname)
75+
server_config = getattr(mod, "config", None)
76+
if server_config is not None:
77+
break
78+
except Exception:
79+
continue
80+
81+
# Determine enabled flag: config -> env DISABLE_* opt-out
82+
cfg_enabled = True if server_config is None else bool(getattr(server_config, "telemetry_enabled", True))
83+
self.enabled = cfg_enabled and not self._is_disabled()
6684

6785
# Telemetry endpoint (Cloud Run default; override via env)
68-
default_ep = "https://unity-mcp-telemetry-375728817078.us-central1.run.app/telemetry/events"
86+
cfg_default = None if server_config is None else getattr(server_config, "telemetry_endpoint", None)
87+
default_ep = cfg_default or "https://unity-mcp-telemetry-375728817078.us-central1.run.app/telemetry/events"
6988
self.default_endpoint = default_ep
7089
self.endpoint = self._validated_endpoint(
7190
os.environ.get("UNITY_MCP_TELEMETRY_ENDPOINT", default_ep),
@@ -142,20 +161,31 @@ def __init__(self):
142161

143162
def _load_persistent_data(self):
144163
"""Load UUID and milestones from disk"""
164+
# Load customer UUID
145165
try:
146-
# Load customer UUID
147166
if self.config.uuid_file.exists():
148-
self._customer_uuid = self.config.uuid_file.read_text().strip()
167+
self._customer_uuid = self.config.uuid_file.read_text(encoding="utf-8").strip() or str(uuid.uuid4())
149168
else:
150169
self._customer_uuid = str(uuid.uuid4())
151-
self.config.uuid_file.write_text(self._customer_uuid)
152-
153-
# Load milestones
154-
if self.config.milestones_file.exists():
155-
self._milestones = json.loads(self.config.milestones_file.read_text())
156-
except Exception as e:
157-
logger.warning(f"Failed to load telemetry data: {e}")
170+
try:
171+
self.config.uuid_file.write_text(self._customer_uuid, encoding="utf-8")
172+
if os.name == "posix":
173+
os.chmod(self.config.uuid_file, 0o600)
174+
except OSError as e:
175+
logger.debug(f"Failed to persist customer UUID: {e}", exc_info=True)
176+
except OSError as e:
177+
logger.debug(f"Failed to load customer UUID: {e}", exc_info=True)
158178
self._customer_uuid = str(uuid.uuid4())
179+
180+
# Load milestones (failure here must not affect UUID)
181+
try:
182+
if self.config.milestones_file.exists():
183+
content = self.config.milestones_file.read_text(encoding="utf-8")
184+
self._milestones = json.loads(content) or {}
185+
if not isinstance(self._milestones, dict):
186+
self._milestones = {}
187+
except (OSError, json.JSONDecodeError, ValueError) as e:
188+
logger.debug(f"Failed to load milestones: {e}", exc_info=True)
159189
self._milestones = {}
160190

161191
def _save_milestones(self):

UnityMcpBridge/UnityMcpServer~/src/tools/manage_script.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,13 +409,14 @@ def delete_script(ctx: Context, uri: str) -> Dict[str, Any]:
409409

410410
@mcp.tool(description=(
411411
"Validate a C# script and return diagnostics.\n\n"
412-
"Args: uri, level=('basic'|'standard').\n"
412+
"Args: uri, level=('basic'|'standard'), include_diagnostics (bool, optional).\n"
413413
"- basic: quick syntax checks.\n"
414414
"- standard: deeper checks (performance hints, common pitfalls).\n"
415+
"- include_diagnostics: when true, returns full diagnostics and summary; default returns counts only.\n"
415416
))
416417
@telemetry_tool("validate_script")
417418
def validate_script(
418-
ctx: Context, uri: str, level: str = "basic"
419+
ctx: Context, uri: str, level: str = "basic", include_diagnostics: bool = False
419420
) -> Dict[str, Any]:
420421
"""Validate a C# script and return diagnostics."""
421422
name, directory = _split_uri(uri)
@@ -431,9 +432,11 @@ def validate_script(
431432
}
432433
resp = send_command_with_retry("manage_script", params)
433434
if isinstance(resp, dict) and resp.get("success"):
434-
diags = resp.get("data", {}).get("diagnostics", [])
435-
warnings = sum(1 for d in diags if str(d.get("severity", "")).lower() in ("warning",))
435+
diags = resp.get("data", {}).get("diagnostics", []) or []
436+
warnings = sum(1 for d in diags if str(d.get("severity", "")).lower() == "warning")
436437
errors = sum(1 for d in diags if str(d.get("severity", "")).lower() in ("error", "fatal"))
438+
if include_diagnostics:
439+
return {"success": True, "data": {"diagnostics": diags, "summary": {"warnings": warnings, "errors": errors}}}
437440
return {"success": True, "data": {"warnings": warnings, "errors": errors}}
438441
return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)}
439442

@@ -573,7 +576,6 @@ def manage_script(
573576

574577
@mcp.tool(description=(
575578
"Get manage_script capabilities (supported ops, limits, and guards).\n\n"
576-
"Args:\n- random_string: required parameter (any string value)\n\n"
577579
"Returns:\n- ops: list of supported structured ops\n- text_ops: list of supported text ops\n- max_edit_payload_bytes: server edit payload cap\n- guards: header/using guard enabled flag\n"
578580
))
579581
@telemetry_tool("manage_script_capabilities")
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import os
2+
import importlib
3+
4+
def test_endpoint_rejects_non_http(tmp_path, monkeypatch):
5+
# Point data dir to temp to avoid touching real files
6+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
7+
monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "file:///etc/passwd")
8+
9+
telemetry = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.telemetry")
10+
importlib.reload(telemetry)
11+
12+
tc = telemetry.TelemetryCollector()
13+
# Should have fallen back to default endpoint
14+
assert tc.config.endpoint == tc.config.default_endpoint
15+
16+
def test_config_preferred_then_env_override(tmp_path, monkeypatch):
17+
# Simulate config telemetry endpoint
18+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
19+
monkeypatch.delenv("UNITY_MCP_TELEMETRY_ENDPOINT", raising=False)
20+
21+
# Patch config.telemetry_endpoint via import mocking
22+
import importlib
23+
cfg_mod = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.config")
24+
old_endpoint = cfg_mod.config.telemetry_endpoint
25+
cfg_mod.config.telemetry_endpoint = "https://example.com/telemetry"
26+
try:
27+
telemetry = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.telemetry")
28+
importlib.reload(telemetry)
29+
tc = telemetry.TelemetryCollector()
30+
assert tc.config.endpoint == "https://example.com/telemetry"
31+
32+
# Env should override config
33+
monkeypatch.setenv("UNITY_MCP_TELEMETRY_ENDPOINT", "https://override.example/ep")
34+
importlib.reload(telemetry)
35+
tc2 = telemetry.TelemetryCollector()
36+
assert tc2.config.endpoint == "https://override.example/ep"
37+
finally:
38+
cfg_mod.config.telemetry_endpoint = old_endpoint
39+
40+
def test_uuid_preserved_on_malformed_milestones(tmp_path, monkeypatch):
41+
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
42+
43+
telemetry = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.telemetry")
44+
importlib.reload(telemetry)
45+
46+
tc1 = telemetry.TelemetryCollector()
47+
first_uuid = tc1._customer_uuid
48+
49+
# Write malformed milestones
50+
tc1.config.milestones_file.write_text("{not-json}", encoding="utf-8")
51+
52+
# Reload collector; UUID should remain same despite bad milestones
53+
importlib.reload(telemetry)
54+
tc2 = telemetry.TelemetryCollector()
55+
assert tc2._customer_uuid == first_uuid
56+

0 commit comments

Comments
 (0)