Skip to content

Commit daef795

Browse files
authored
Merge pull request #72 from golf-mcp/asch/telemetry-error-differenciation
Asch/telemetry error differentiation
2 parents c7ed47e + af30e57 commit daef795

File tree

7 files changed

+179
-59
lines changed

7 files changed

+179
-59
lines changed

.github/SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
| Version | Supported |
66
| ------- | ------------------ |
7-
| 0.1.12 | :white_check_mark: |
7+
| 0.1.13 | :white_check_mark: |
88

99

1010
## Reporting a Vulnerability

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "golf-mcp"
7-
version = "0.1.12"
7+
version = "0.1.13"
88
description = "Framework for building MCP servers"
99
authors = [
1010
{name = "Antoni Gmitruk", email = "antoni@golf.dev"}
@@ -64,7 +64,7 @@ golf = ["examples/**/*"]
6464

6565
[tool.poetry]
6666
name = "golf-mcp"
67-
version = "0.1.12"
67+
version = "0.1.13"
6868
description = "Framework for building MCP servers with zero boilerplate"
6969
authors = ["Antoni Gmitruk <antoni@golf.dev>"]
7070
license = "Apache-2.0"

src/golf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.12"
1+
__version__ = "0.1.13"

src/golf/cli/main.py

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
set_telemetry_enabled,
1616
shutdown,
1717
track_event,
18+
track_detailed_error,
19+
_detect_execution_environment,
1820
)
1921

2022
# Create console for rich output
@@ -156,16 +158,12 @@ def build_dev(
156158
# Track successful build with environment
157159
track_event("cli_build_success", {"success": True, "environment": "dev"})
158160
except Exception as e:
159-
error_type = type(e).__name__
160-
error_message = str(e)
161-
track_event(
161+
track_detailed_error(
162162
"cli_build_failed",
163-
{
164-
"success": False,
165-
"environment": "dev",
166-
"error_type": error_type,
167-
"error_message": error_message,
168-
},
163+
e,
164+
context="Development build with environment variables",
165+
operation="build_dev",
166+
additional_props={"environment": "dev", "copy_env": True}
169167
)
170168
raise
171169

@@ -212,16 +210,12 @@ def build_prod(
212210
# Track successful build with environment
213211
track_event("cli_build_success", {"success": True, "environment": "prod"})
214212
except Exception as e:
215-
error_type = type(e).__name__
216-
error_message = str(e)
217-
track_event(
213+
track_detailed_error(
218214
"cli_build_failed",
219-
{
220-
"success": False,
221-
"environment": "prod",
222-
"error_type": error_type,
223-
"error_message": error_message,
224-
},
215+
e,
216+
context="Production build without environment variables",
217+
operation="build_prod",
218+
additional_props={"environment": "prod", "copy_env": False}
225219
)
226220
raise
227221

@@ -282,18 +276,15 @@ def run(
282276

283277
build_project(project_root, settings, dist_dir)
284278
except Exception as e:
285-
error_type = type(e).__name__
286-
error_message = str(e)
287279
console.print(
288-
f"[bold red]Error building project:[/bold red] {error_message}"
280+
f"[bold red]Error building project:[/bold red] {str(e)}"
289281
)
290-
track_event(
282+
track_detailed_error(
291283
"cli_run_failed",
292-
{
293-
"success": False,
294-
"error_type": f"BuildError.{error_type}",
295-
"error_message": error_message,
296-
},
284+
e,
285+
context="Auto-build before running server",
286+
operation="auto_build_before_run",
287+
additional_props={"auto_build": True}
297288
)
298289
raise
299290
else:
@@ -325,32 +316,56 @@ def run(
325316
port=port,
326317
)
327318

328-
# Track based on return code
319+
# Track based on return code with better categorization
329320
if return_code == 0:
330321
track_event("cli_run_success", {"success": True})
322+
elif return_code in [130, 143, 137, 2]:
323+
# Intentional shutdowns (not errors):
324+
# 130: Ctrl+C (SIGINT)
325+
# 143: SIGTERM (graceful shutdown, e.g., Kubernetes, Docker)
326+
# 137: SIGKILL (forced shutdown)
327+
# 2: General interrupt/graceful shutdown
328+
shutdown_type = {
329+
130: "UserInterrupt",
330+
143: "GracefulShutdown",
331+
137: "ForcedShutdown",
332+
2: "Interrupt"
333+
}.get(return_code, "GracefulShutdown")
334+
335+
track_event(
336+
"cli_run_shutdown",
337+
{
338+
"success": True, # Not an error
339+
"shutdown_type": shutdown_type,
340+
"exit_code": return_code,
341+
},
342+
)
331343
else:
344+
# Actual errors (unexpected exit codes)
332345
track_event(
333346
"cli_run_failed",
334347
{
335348
"success": False,
336-
"error_type": "NonZeroExit",
337-
"error_message": f"Server exited with code {return_code}",
349+
"error_type": "UnexpectedExit",
350+
"error_message": f"Server process exited unexpectedly with code {return_code}",
351+
"exit_code": return_code,
352+
"operation": "server_process_execution",
353+
"context": "Server process terminated with unexpected exit code",
354+
# Add execution environment context
355+
"execution_env": _detect_execution_environment(),
338356
},
339357
)
340358

341359
# Exit with the same code as the server
342360
if return_code != 0:
343361
raise typer.Exit(code=return_code)
344362
except Exception as e:
345-
error_type = type(e).__name__
346-
error_message = str(e)
347-
track_event(
363+
track_detailed_error(
348364
"cli_run_failed",
349-
{
350-
"success": False,
351-
"error_type": error_type,
352-
"error_message": error_message,
353-
},
365+
e,
366+
context="Server execution or startup failure",
367+
operation="run_server_execution",
368+
additional_props={"has_dist_dir": dist_dir.exists() if dist_dir else False}
354369
)
355370
raise
356371

src/golf/commands/run.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,24 @@ def run_server(
6464
env=env,
6565
)
6666

67+
# Provide more context about the exit
68+
if process.returncode == 0:
69+
console.print("[green]Server stopped successfully[/green]")
70+
elif process.returncode == 130:
71+
console.print("[yellow]Server stopped by user interrupt (Ctrl+C)[/yellow]")
72+
elif process.returncode == 143:
73+
console.print("[yellow]Server stopped by SIGTERM (graceful shutdown)[/yellow]")
74+
elif process.returncode == 137:
75+
console.print("[yellow]Server stopped by SIGKILL (forced shutdown)[/yellow]")
76+
elif process.returncode in [1, 2]:
77+
console.print(f"[red]Server exited with error code {process.returncode}[/red]")
78+
else:
79+
console.print(f"[orange]Server exited with code {process.returncode}[/orange]")
80+
6781
return process.returncode
6882
except KeyboardInterrupt:
69-
console.print("\n[yellow]Server stopped by user[/yellow]")
70-
return 0
83+
console.print("\n[yellow]Server stopped by user (Ctrl+C)[/yellow]")
84+
return 130 # Standard exit code for SIGINT
7185
except Exception as e:
7286
console.print(f"\n[bold red]Error running server:[/bold red] {e}")
7387
return 1

src/golf/core/builder.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,23 @@ def build_project(
950950
import traceback
951951

952952
console.print(f"[red]{traceback.format_exc()}[/red]")
953+
954+
# Track detailed error for pre_build.py execution failures
955+
try:
956+
from golf.core.telemetry import track_detailed_error
957+
track_detailed_error(
958+
"build_pre_build_failed",
959+
e,
960+
context="Executing pre_build.py configuration script",
961+
operation="pre_build_execution",
962+
additional_props={
963+
"file_path": str(pre_build_path.relative_to(project_path)),
964+
"build_env": build_env,
965+
}
966+
)
967+
except Exception:
968+
# Don't let telemetry errors break the build
969+
pass
953970

954971
# Clear the output directory if it exists
955972
if output_dir.exists():

src/golf/core/telemetry.py

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
281281
"command_type",
282282
"error_type",
283283
"error_message",
284+
"shutdown_type",
285+
"exit_code",
284286
}
285287
for key in safe_keys:
286288
if key in properties:
@@ -326,26 +328,98 @@ def track_command(
326328
track_event(f"cli_{command}", properties)
327329

328330

329-
def _sanitize_error_message(message: str) -> str:
330-
"""Sanitize error message to remove sensitive information.
331+
def track_detailed_error(
332+
event_name: str,
333+
error: Exception,
334+
context: str | None = None,
335+
operation: str | None = None,
336+
additional_props: dict[str, Any] | None = None,
337+
) -> None:
338+
"""Track a detailed error with enhanced debugging information.
331339
332340
Args:
333-
message: Raw error message
334-
335-
Returns:
336-
Sanitized error message
341+
event_name: Name of the error event (e.g., "cli_run_failed", "cli_build_failed")
342+
error: The exception that occurred
343+
context: Additional context about where the error occurred
344+
operation: The specific operation that failed
345+
additional_props: Additional properties to include
337346
"""
347+
import traceback
348+
import time
349+
350+
properties = {
351+
"success": False,
352+
"error_type": type(error).__name__,
353+
"error_message": _sanitize_error_message(str(error)),
354+
"timestamp": int(time.time()),
355+
}
356+
357+
# Add operation context
358+
if operation:
359+
properties["operation"] = operation
360+
if context:
361+
properties["context"] = context
362+
363+
# Add sanitized stack trace for debugging
364+
try:
365+
tb_lines = traceback.format_exception(type(error), error, error.__traceback__)
366+
# Get the last few frames (most relevant) and sanitize them
367+
relevant_frames = tb_lines[-3:] if len(tb_lines) > 3 else tb_lines
368+
sanitized_trace = []
369+
370+
for frame in relevant_frames:
371+
# Sanitize file paths in stack trace
372+
sanitized_frame = _sanitize_error_message(frame.strip())
373+
# Further sanitize common traceback patterns
374+
sanitized_frame = sanitized_frame.replace('File "[PATH]', 'File "[PATH]')
375+
sanitized_trace.append(sanitized_frame)
376+
377+
properties["stack_trace"] = " | ".join(sanitized_trace)
378+
379+
# Add the specific line that caused the error if available
380+
if hasattr(error, '__traceback__') and error.__traceback__:
381+
tb = error.__traceback__
382+
while tb.tb_next:
383+
tb = tb.tb_next
384+
properties["error_line"] = tb.tb_lineno
385+
386+
except Exception:
387+
# Don't fail if we can't capture stack trace
388+
pass
389+
390+
# Add system context for debugging
391+
try:
392+
properties["python_executable"] = _sanitize_error_message(platform.python_implementation())
393+
properties["platform_detail"] = platform.platform()[:50] # Limit length
394+
except Exception:
395+
pass
396+
397+
# Merge additional properties
398+
if additional_props:
399+
# Only include safe additional properties
400+
safe_additional_keys = {
401+
"exit_code", "shutdown_type", "environment", "template",
402+
"build_env", "transport", "component_count", "file_path",
403+
"component_type", "validation_error", "config_error"
404+
}
405+
for key, value in additional_props.items():
406+
if key in safe_additional_keys:
407+
properties[key] = value
408+
409+
track_event(event_name, properties)
410+
411+
def _sanitize_error_message(message: str) -> str:
412+
"""Sanitize error messages to remove sensitive information."""
338413
import re
339414

340-
# Remove absolute file paths but keep the filename
341-
# Unix-style paths
342-
message = re.sub(
343-
r'/(?:Users|home|var|tmp|opt|usr|etc)/[^\s"\']+/([^/\s"\']+)', r"\1", message
344-
)
345-
# Windows-style paths
346-
message = re.sub(r'[A-Za-z]:\\[^\s"\']+\\([^\\s"\']+)', r"\1", message)
347-
# Generic path pattern (catches remaining paths)
348-
message = re.sub(r'(?:^|[\s"])(/[^\s"\']+/)+([^/\s"\']+)', r"\2", message)
415+
# Remove file paths but preserve filenames
416+
# Match paths with directories and capture the filename
417+
# Unix style: /path/to/file.py -> file.py
418+
message = re.sub(r"(/[^/\s]+)+/([^/\s]+)", r"\2", message)
419+
# Windows style: C:\path\to\file.py -> file.py
420+
message = re.sub(r"([A-Za-z]:\\[^\\]+\\)+([^\\]+)", r"\2", message)
421+
# Remaining absolute paths without filename
422+
message = re.sub(r"[/\\][^\s]*[/\\]", "[PATH]/", message)
349423

350424
# Remove potential API keys or tokens (common patterns)
351425
# Generic API keys (20+ alphanumeric with underscores/hyphens)

0 commit comments

Comments
 (0)