Skip to content

Commit f127024

Browse files
committed
telemetry: enable tool_execution across tools via strict, async-aware decorator; add endpoint env override + urllib fallback; enrich OS fields; fix TelemetryHelper invocation
1 parent 81dcd69 commit f127024

File tree

13 files changed

+152
-60
lines changed

13 files changed

+152
-60
lines changed

UnityMcpBridge/Editor/Helpers/TelemetryHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public static void RecordBridgeStartup()
118118
RecordEvent("bridge_startup", new Dictionary<string, object>
119119
{
120120
["bridge_version"] = "3.0.2",
121-
["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode
121+
["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode()
122122
});
123123
}
124124

UnityMcpBridge/UnityMcpServer~/src/telemetry.py

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import json
1010
import time
1111
import os
12+
import sys
13+
import platform
1214
import logging
1315
from enum import Enum
1416
from dataclasses import dataclass, asdict
@@ -61,8 +63,11 @@ def __init__(self):
6163
# Check environment variables for opt-out
6264
self.enabled = not self._is_disabled()
6365

64-
# Telemetry endpoint - hardcoded to Coplay production API
65-
self.endpoint = "https://api-prod.coplay.dev/telemetry/events"
66+
# Telemetry endpoint (Cloud Run default; override via env)
67+
self.endpoint = os.environ.get(
68+
"UNITY_MCP_TELEMETRY_ENDPOINT",
69+
"https://unity-mcp-telemetry-375728817078.us-central1.run.app/telemetry/events"
70+
)
6671

6772
# Local storage for UUID and milestones
6873
self.data_dir = self._get_data_directory()
@@ -172,9 +177,7 @@ def record(self,
172177
if not self.config.enabled:
173178
return
174179

175-
if not HAS_HTTPX:
176-
logger.debug("Telemetry disabled: httpx not available")
177-
return
180+
# Allow fallback sender when httpx is unavailable (no early return)
178181

179182
record = TelemetryRecord(
180183
record_type=record_type,
@@ -196,34 +199,63 @@ def record(self,
196199
def _send_telemetry(self, record: TelemetryRecord):
197200
"""Send telemetry data to endpoint"""
198201
try:
202+
# System fingerprint (top-level remains concise; details stored in data JSON)
203+
_platform = platform.system() # 'Darwin' | 'Linux' | 'Windows'
204+
_source = sys.platform # 'darwin' | 'linux' | 'win32'
205+
_platform_detail = f"{_platform} {platform.release()} ({platform.machine()})"
206+
_python_version = platform.python_version()
207+
208+
# Enrich data JSON so BigQuery stores detailed fields without schema change
209+
enriched_data = dict(record.data or {})
210+
enriched_data.setdefault("platform_detail", _platform_detail)
211+
enriched_data.setdefault("python_version", _python_version)
212+
199213
payload = {
200214
"record": record.record_type.value,
201215
"timestamp": record.timestamp,
202216
"customer_uuid": record.customer_uuid,
203217
"session_id": record.session_id,
204-
"data": record.data,
218+
"data": enriched_data,
205219
"version": "3.0.2", # Unity MCP version
206-
"platform": os.name
220+
"platform": _platform,
221+
"source": _source,
207222
}
208-
223+
209224
if record.milestone:
210225
payload["milestone"] = record.milestone.value
211-
212-
if not httpx:
213-
return
214-
215-
with httpx.Client(timeout=self.config.timeout) as client:
216-
response = client.post(self.config.endpoint, json=payload)
217-
218-
if response.status_code == 200:
219-
logger.debug(f"Telemetry sent: {record.record_type}")
220-
else:
221-
logger.debug(f"Telemetry failed: HTTP {response.status_code}")
222-
226+
227+
# Prefer httpx when available; otherwise fall back to urllib
228+
if httpx:
229+
with httpx.Client(timeout=self.config.timeout) as client:
230+
response = client.post(self.config.endpoint, json=payload)
231+
if response.status_code == 200:
232+
logger.debug(f"Telemetry sent: {record.record_type}")
233+
else:
234+
logger.debug(f"Telemetry failed: HTTP {response.status_code}")
235+
else:
236+
import urllib.request
237+
import urllib.error
238+
data_bytes = json.dumps(payload).encode("utf-8")
239+
req = urllib.request.Request(
240+
self.config.endpoint,
241+
data=data_bytes,
242+
headers={"Content-Type": "application/json"},
243+
method="POST",
244+
)
245+
try:
246+
with urllib.request.urlopen(req, timeout=self.config.timeout) as resp:
247+
if 200 <= resp.getcode() < 300:
248+
logger.debug(f"Telemetry sent (urllib): {record.record_type}")
249+
else:
250+
logger.debug(f"Telemetry failed (urllib): HTTP {resp.getcode()}")
251+
except urllib.error.URLError as ue:
252+
logger.debug(f"Telemetry send failed (urllib): {ue}")
253+
223254
except Exception as e:
224255
# Never let telemetry errors interfere with app functionality
225256
logger.debug(f"Telemetry send failed: {e}")
226257

258+
227259
# Global telemetry instance
228260
_telemetry_collector: Optional[TelemetryCollector] = None
229261

UnityMcpBridge/UnityMcpServer~/src/telemetry_decorator.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,66 @@
44

55
import functools
66
import time
7+
import inspect
8+
import logging
79
from typing import Callable, Any
810
from telemetry import record_tool_usage, record_milestone, MilestoneType
911

12+
_log = logging.getLogger("unity-mcp-telemetry")
13+
_decorator_log_count = 0
14+
1015
def telemetry_tool(tool_name: str):
1116
"""Decorator to add telemetry tracking to MCP tools"""
1217
def decorator(func: Callable) -> Callable:
1318
@functools.wraps(func)
14-
def wrapper(*args, **kwargs) -> Any:
19+
def _sync_wrapper(*args, **kwargs) -> Any:
1520
start_time = time.time()
1621
success = False
1722
error = None
18-
1923
try:
24+
global _decorator_log_count
25+
if _decorator_log_count < 10:
26+
_log.info(f"telemetry_decorator sync: tool={tool_name}")
27+
_decorator_log_count += 1
2028
result = func(*args, **kwargs)
2129
success = True
22-
23-
# Record tool-specific milestones
2430
if tool_name == "manage_script" and kwargs.get("action") == "create":
2531
record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
2632
elif tool_name.startswith("manage_scene"):
2733
record_milestone(MilestoneType.FIRST_SCENE_MODIFICATION)
28-
29-
# Record general first tool usage
3034
record_milestone(MilestoneType.FIRST_TOOL_USAGE)
31-
3235
return result
33-
3436
except Exception as e:
3537
error = str(e)
3638
raise
3739
finally:
3840
duration_ms = (time.time() - start_time) * 1000
3941
record_tool_usage(tool_name, success, duration_ms, error)
40-
41-
return wrapper
42+
43+
@functools.wraps(func)
44+
async def _async_wrapper(*args, **kwargs) -> Any:
45+
start_time = time.time()
46+
success = False
47+
error = None
48+
try:
49+
global _decorator_log_count
50+
if _decorator_log_count < 10:
51+
_log.info(f"telemetry_decorator async: tool={tool_name}")
52+
_decorator_log_count += 1
53+
result = await func(*args, **kwargs)
54+
success = True
55+
if tool_name == "manage_script" and kwargs.get("action") == "create":
56+
record_milestone(MilestoneType.FIRST_SCRIPT_CREATION)
57+
elif tool_name.startswith("manage_scene"):
58+
record_milestone(MilestoneType.FIRST_SCENE_MODIFICATION)
59+
record_milestone(MilestoneType.FIRST_TOOL_USAGE)
60+
return result
61+
except Exception as e:
62+
error = str(e)
63+
raise
64+
finally:
65+
duration_ms = (time.time() - start_time) * 1000
66+
record_tool_usage(tool_name, success, duration_ms, error)
67+
68+
return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper
4269
return decorator

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
from config import config
88
import time
99

10+
from telemetry_decorator import telemetry_tool
11+
1012
def register_execute_menu_item_tools(mcp: FastMCP):
1113
"""Registers the execute_menu_item tool with the MCP server."""
1214

1315
@mcp.tool()
16+
@telemetry_tool("execute_menu_item")
1417
async def execute_menu_item(
15-
ctx: Context,
18+
ctx: Any,
1619
menu_path: str,
1720
action: str = 'execute',
1821
parameters: Dict[str, Any] = None,

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
from config import config
1010
import time
1111

12+
from telemetry_decorator import telemetry_tool
13+
1214
def register_manage_asset_tools(mcp: FastMCP):
1315
"""Registers the manage_asset tool with the MCP server."""
1416

1517
@mcp.tool()
18+
@telemetry_tool("manage_asset")
1619
async def manage_asset(
17-
ctx: Context,
20+
ctx: Any,
1821
action: str,
1922
path: str,
2023
asset_type: str = None,

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44
from unity_connection import get_unity_connection, send_command_with_retry
55
from config import config
66

7+
from telemetry_decorator import telemetry_tool
8+
from telemetry import is_telemetry_enabled, record_tool_usage
9+
710
def register_manage_editor_tools(mcp: FastMCP):
811
"""Register all editor management tools with the MCP server."""
912

1013
@mcp.tool()
14+
@telemetry_tool("manage_editor")
1115
def manage_editor(
12-
ctx: Context,
16+
ctx: Any,
1317
action: str,
1418
wait_for_completion: bool = None,
1519
# --- Parameters for specific actions ---
@@ -28,6 +32,13 @@ def manage_editor(
2832
Dictionary with operation results ('success', 'message', 'data').
2933
"""
3034
try:
35+
# Diagnostics: quick telemetry checks
36+
if action == "telemetry_status":
37+
return {"success": True, "telemetry_enabled": is_telemetry_enabled()}
38+
39+
if action == "telemetry_ping":
40+
record_tool_usage("diagnostic_ping", True, 1.0, None)
41+
return {"success": True, "message": "telemetry ping queued"}
3142
# Prepare parameters, removing None values
3243
params = {
3344
"action": action,

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
from config import config
55
import time
66

7+
from telemetry_decorator import telemetry_tool
8+
79
def register_manage_gameobject_tools(mcp: FastMCP):
810
"""Register all GameObject management tools with the MCP server."""
911

1012
@mcp.tool()
13+
@telemetry_tool("manage_gameobject")
1114
def manage_gameobject(
12-
ctx: Context,
15+
ctx: Any,
1316
action: str,
1417
target: str = None, # GameObject identifier by name or path
1518
search_method: str = None,

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
from config import config
55
import time
66

7+
from telemetry_decorator import telemetry_tool
8+
79
def register_manage_scene_tools(mcp: FastMCP):
810
"""Register all scene management tools with the MCP server."""
911

1012
@mcp.tool()
13+
@telemetry_tool("manage_scene")
1114
def manage_scene(
12-
ctx: Context,
15+
ctx: Any,
1316
action: str,
1417
name: str,
1518
path: str,

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

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,8 @@
55
import os
66
from urllib.parse import urlparse, unquote
77

8-
try:
9-
from telemetry_decorator import telemetry_tool
10-
from telemetry import record_milestone, MilestoneType
11-
HAS_TELEMETRY = True
12-
except ImportError:
13-
HAS_TELEMETRY = False
14-
def telemetry_tool(tool_name: str):
15-
def decorator(func):
16-
return func
17-
return decorator
8+
from telemetry_decorator import telemetry_tool
9+
from telemetry import record_milestone, MilestoneType
1810

1911
def register_manage_script_tools(mcp: FastMCP):
2012
"""Register all script management tools with the MCP server."""
@@ -92,7 +84,7 @@ def _split_uri(uri: str) -> tuple[str, str]:
9284
))
9385
@telemetry_tool("apply_text_edits")
9486
def apply_text_edits(
95-
ctx: Context,
87+
ctx: Any,
9688
uri: str,
9789
edits: List[Dict[str, Any]],
9890
precondition_sha256: str | None = None,
@@ -359,7 +351,7 @@ def _flip_async():
359351
))
360352
@telemetry_tool("create_script")
361353
def create_script(
362-
ctx: Context,
354+
ctx: Any,
363355
path: str,
364356
contents: str = "",
365357
script_type: str | None = None,
@@ -397,7 +389,8 @@ def create_script(
397389
"Args: uri (unity://path/... or file://... or Assets/...).\n"
398390
"Rules: Target must resolve under Assets/.\n"
399391
))
400-
def delete_script(ctx: Context, uri: str) -> Dict[str, Any]:
392+
@telemetry_tool("delete_script")
393+
def delete_script(ctx: Any, uri: str) -> Dict[str, Any]:
401394
"""Delete a C# script by URI."""
402395
name, directory = _split_uri(uri)
403396
if not directory or directory.split("/")[0].lower() != "assets":
@@ -412,8 +405,9 @@ def delete_script(ctx: Context, uri: str) -> Dict[str, Any]:
412405
"- basic: quick syntax checks.\n"
413406
"- standard: deeper checks (performance hints, common pitfalls).\n"
414407
))
408+
@telemetry_tool("validate_script")
415409
def validate_script(
416-
ctx: Context, uri: str, level: str = "basic"
410+
ctx: Any, uri: str, level: str = "basic"
417411
) -> Dict[str, Any]:
418412
"""Validate a C# script and return diagnostics."""
419413
name, directory = _split_uri(uri)
@@ -443,7 +437,7 @@ def validate_script(
443437
))
444438
@telemetry_tool("manage_script")
445439
def manage_script(
446-
ctx: Context,
440+
ctx: Any,
447441
action: str,
448442
name: str,
449443
path: str,
@@ -573,7 +567,8 @@ def manage_script(
573567
"Get manage_script capabilities (supported ops, limits, and guards).\n\n"
574568
"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"
575569
))
576-
def manage_script_capabilities(ctx: Context) -> Dict[str, Any]:
570+
@telemetry_tool("manage_script_capabilities")
571+
def manage_script_capabilities(ctx: Any) -> Dict[str, Any]:
577572
try:
578573
# Keep in sync with server/Editor ManageScript implementation
579574
ops = [
@@ -600,7 +595,8 @@ def manage_script_capabilities(ctx: Context) -> Dict[str, Any]:
600595
"Args: uri (unity://path/Assets/... or file://... or Assets/...).\n"
601596
"Returns: {sha256, lengthBytes, lastModifiedUtc, uri, path}."
602597
))
603-
def get_sha(ctx: Context, uri: str) -> Dict[str, Any]:
598+
@telemetry_tool("get_sha")
599+
def get_sha(ctx: Any, uri: str) -> Dict[str, Any]:
604600
"""Return SHA256 and basic metadata for a script."""
605601
try:
606602
name, directory = _split_uri(uri)

0 commit comments

Comments
 (0)