Skip to content

Commit 89714d0

Browse files
committed
telemetry: record sub_action for tool executions; decorator extracts 'action'; add tests for keyword/positional extraction
1 parent 2fd74f5 commit 89714d0

File tree

3 files changed

+120
-4
lines changed

3 files changed

+120
-4
lines changed

UnityMcpBridge/UnityMcpServer~/src/telemetry.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,13 +356,28 @@ def record_milestone(milestone: MilestoneType, data: Optional[Dict[str, Any]] =
356356
"""Convenience function to record a milestone"""
357357
return get_telemetry().record_milestone(milestone, data)
358358

359-
def record_tool_usage(tool_name: str, success: bool, duration_ms: float, error: Optional[str] = None):
360-
"""Record tool usage telemetry"""
359+
def record_tool_usage(tool_name: str, success: bool, duration_ms: float, error: Optional[str] = None, sub_action: Optional[str] = None):
360+
"""Record tool usage telemetry
361+
362+
Args:
363+
tool_name: Name of the tool invoked (e.g., 'manage_scene').
364+
success: Whether the tool completed successfully.
365+
duration_ms: Execution duration in milliseconds.
366+
error: Optional error message (truncated if present).
367+
sub_action: Optional sub-action/operation within the tool (e.g., 'get_hierarchy').
368+
"""
361369
data = {
362370
"tool_name": tool_name,
363371
"success": success,
364372
"duration_ms": round(duration_ms, 2)
365373
}
374+
375+
if sub_action is not None:
376+
try:
377+
data["sub_action"] = str(sub_action)
378+
except Exception:
379+
# Ensure telemetry is never disruptive
380+
data["sub_action"] = "unknown"
366381

367382
if error:
368383
data["error"] = str(error)[:200] # Limit error message length

UnityMcpBridge/UnityMcpServer~/src/telemetry_decorator.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ def _sync_wrapper(*args, **kwargs) -> Any:
2020
start_time = time.time()
2121
success = False
2222
error = None
23+
# Extract sub-action (e.g., 'get_hierarchy') from bound args when available
24+
sub_action = None
25+
try:
26+
sig = inspect.signature(func)
27+
bound = sig.bind_partial(*args, **kwargs)
28+
bound.apply_defaults()
29+
sub_action = bound.arguments.get("action")
30+
except Exception:
31+
sub_action = None
2332
try:
2433
global _decorator_log_count
2534
if _decorator_log_count < 10:
@@ -38,13 +47,22 @@ def _sync_wrapper(*args, **kwargs) -> Any:
3847
raise
3948
finally:
4049
duration_ms = (time.time() - start_time) * 1000
41-
record_tool_usage(tool_name, success, duration_ms, error)
50+
record_tool_usage(tool_name, success, duration_ms, error, sub_action=sub_action)
4251

4352
@functools.wraps(func)
4453
async def _async_wrapper(*args, **kwargs) -> Any:
4554
start_time = time.time()
4655
success = False
4756
error = None
57+
# Extract sub-action (e.g., 'get_hierarchy') from bound args when available
58+
sub_action = None
59+
try:
60+
sig = inspect.signature(func)
61+
bound = sig.bind_partial(*args, **kwargs)
62+
bound.apply_defaults()
63+
sub_action = bound.arguments.get("action")
64+
except Exception:
65+
sub_action = None
4866
try:
4967
global _decorator_log_count
5068
if _decorator_log_count < 10:
@@ -63,7 +81,7 @@ async def _async_wrapper(*args, **kwargs) -> Any:
6381
raise
6482
finally:
6583
duration_ms = (time.time() - start_time) * 1000
66-
record_tool_usage(tool_name, success, duration_ms, error)
84+
record_tool_usage(tool_name, success, duration_ms, error, sub_action=sub_action)
6785

6886
return _async_wrapper if inspect.iscoroutinefunction(func) else _sync_wrapper
6987
return decorator

tests/test_telemetry_subaction.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import importlib
2+
3+
4+
def _get_decorator_module():
5+
# Import the telemetry_decorator module from the Unity MCP server src
6+
mod = importlib.import_module("UnityMcpBridge.UnityMcpServer~.src.telemetry_decorator")
7+
return mod
8+
9+
10+
def test_subaction_extracted_from_keyword(monkeypatch):
11+
td = _get_decorator_module()
12+
13+
captured = {}
14+
15+
def fake_record_tool_usage(tool_name, success, duration_ms, error, sub_action=None):
16+
captured["tool_name"] = tool_name
17+
captured["success"] = success
18+
captured["error"] = error
19+
captured["sub_action"] = sub_action
20+
21+
# Silence milestones/logging in test
22+
monkeypatch.setattr(td, "record_tool_usage", fake_record_tool_usage)
23+
monkeypatch.setattr(td, "record_milestone", lambda *a, **k: None)
24+
monkeypatch.setattr(td, "_decorator_log_count", 999)
25+
26+
def dummy_tool(ctx, action: str, name: str = ""):
27+
return {"success": True, "name": name}
28+
29+
wrapped = td.telemetry_tool("manage_scene")(dummy_tool)
30+
31+
resp = wrapped(None, action="get_hierarchy", name="Sample")
32+
assert resp["success"] is True
33+
assert captured["tool_name"] == "manage_scene"
34+
assert captured["success"] is True
35+
assert captured["error"] is None
36+
assert captured["sub_action"] == "get_hierarchy"
37+
38+
39+
def test_subaction_extracted_from_positionals(monkeypatch):
40+
td = _get_decorator_module()
41+
42+
captured = {}
43+
44+
def fake_record_tool_usage(tool_name, success, duration_ms, error, sub_action=None):
45+
captured["tool_name"] = tool_name
46+
captured["sub_action"] = sub_action
47+
48+
monkeypatch.setattr(td, "record_tool_usage", fake_record_tool_usage)
49+
monkeypatch.setattr(td, "record_milestone", lambda *a, **k: None)
50+
monkeypatch.setattr(td, "_decorator_log_count", 999)
51+
52+
def dummy_tool(ctx, action: str, name: str = ""):
53+
return True
54+
55+
wrapped = td.telemetry_tool("manage_scene")(dummy_tool)
56+
57+
_ = wrapped(None, "save", "MyScene")
58+
assert captured["tool_name"] == "manage_scene"
59+
assert captured["sub_action"] == "save"
60+
61+
62+
def test_subaction_none_when_not_present(monkeypatch):
63+
td = _get_decorator_module()
64+
65+
captured = {}
66+
67+
def fake_record_tool_usage(tool_name, success, duration_ms, error, sub_action=None):
68+
captured["tool_name"] = tool_name
69+
captured["sub_action"] = sub_action
70+
71+
monkeypatch.setattr(td, "record_tool_usage", fake_record_tool_usage)
72+
monkeypatch.setattr(td, "record_milestone", lambda *a, **k: None)
73+
monkeypatch.setattr(td, "_decorator_log_count", 999)
74+
75+
def dummy_tool_without_action(ctx, name: str):
76+
return 123
77+
78+
wrapped = td.telemetry_tool("apply_text_edits")(dummy_tool_without_action)
79+
_ = wrapped(None, name="X")
80+
assert captured["tool_name"] == "apply_text_edits"
81+
assert captured["sub_action"] is None
82+
83+

0 commit comments

Comments
 (0)