Files
tracer-cloud--opensre/surfaces/interactive_shell/ui/investigation_outcome.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

161 lines
5.0 KiB
Python

"""Structured investigation run outcomes for terminal UX and PostHog analytics."""
from __future__ import annotations
import re
import traceback
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
from platform.common.errors import OpenSREError
InvestigationStatus = Literal["completed", "failed", "cancelled"]
FailureCategory = Literal[
"config",
"integration",
"timeout",
"user_cancelled",
"k8s_api",
"llm",
"unknown",
]
_INTEGRATION_KEYWORDS: tuple[tuple[str, str], ...] = (
("grafana", "grafana"),
("loki", "grafana"),
("mimir", "grafana"),
("tempo", "grafana"),
("datadog", "datadog"),
("sentry", "sentry"),
("jenkins", "jenkins"),
("kubernetes", "k8s"),
("k8s", "k8s"),
("kubectl", "k8s"),
("splunk", "splunk"),
("honeycomb", "honeycomb"),
("coralogix", "coralogix"),
("posthog", "posthog"),
("github", "github"),
("argocd", "argocd"),
("pagerduty", "pagerduty"),
)
@dataclass(frozen=True, slots=True)
class InvestigationOutcome:
"""Facts from one foreground investigation run."""
status: InvestigationStatus
target: str = ""
investigation_id: str = ""
final_state: dict[str, Any] | None = None
error_message: str = ""
error_detail: str = ""
failure_category: FailureCategory = "unknown"
integration_involved: str = ""
integration_failure_message: str = ""
llm_model: str = ""
llm_provider: str = ""
llm_input_tokens: int = 0
llm_output_tokens: int = 0
duration_ms: int = 0
def normalize_investigation_target(raw_target: str, *, path: Path | None = None) -> str:
"""Return a stable analytics slug for an investigation target."""
if path is not None:
return path.name or path.stem or str(path)
stripped = raw_target.strip()
if not stripped:
return "investigation"
lowered = stripped.lower()
for prefix in ("sample:", "template:"):
if lowered.startswith(prefix):
return lowered[len(prefix) :].strip() or "investigation"
if "/" in stripped or "\\" in stripped:
return Path(stripped).name or stripped
collapsed = re.sub(r"\s+", " ", stripped)
if len(collapsed) > 80:
return f"{collapsed[:77]}…"
return collapsed
def _integration_from_message(message: str) -> tuple[str, str]:
lowered = message.lower()
for keyword, service in _INTEGRATION_KEYWORDS:
if keyword in lowered:
detail = message.strip()
if len(detail) > 200:
detail = f"{detail[:197]}…"
return service, detail
return "", ""
def classify_investigation_failure(
exc: BaseException,
) -> tuple[FailureCategory, str, str]:
"""Map an exception to category, integration service, and integration detail."""
message = str(exc).strip()
if isinstance(exc, TimeoutError):
return "timeout", "", ""
if isinstance(exc, KeyboardInterrupt):
return "user_cancelled", "", ""
integration, integration_detail = _integration_from_message(message)
lowered = message.lower()
if integration:
return "integration", integration, integration_detail
if any(token in lowered for token in ("kubernetes", "k8s", "kubectl", "pod ", "deployment ")):
return "k8s_api", "k8s", message[:200]
if any(
token in lowered
for token in ("context length", "token limit", "llm", "model", "anthropic", "openai")
):
return "llm", "", message[:200]
if any(token in lowered for token in ("config", "credential", "not configured", "missing api")):
return "config", "", message[:200]
if isinstance(exc, OpenSREError):
return "config", integration, integration_detail or message[:200]
return "unknown", integration, integration_detail
def failure_detail_from_exception(exc: BaseException) -> str:
"""Best-effort truncated detail for analytics (not user-facing)."""
return truncate_failure_detail("".join(traceback.format_exception_only(exc)).strip())
def truncate_failure_detail(text: str, *, max_chars: int = 500) -> str:
cleaned = text.strip()
if len(cleaned) <= max_chars:
return cleaned
return f"{cleaned[: max_chars - 20].rstrip()}… [truncated]"
def user_facing_error_message(exc: BaseException, *, max_lines: int = 3) -> str:
"""Compact user-facing error text for analytics payloads."""
if isinstance(exc, OpenSREError):
parts = [exc.message.strip()]
if exc.suggestion:
parts.append(f"Suggestion: {exc.suggestion.strip()}")
text = "\n".join(part for part in parts if part)
else:
text = str(exc).strip()
lines = [line.strip() for line in text.splitlines() if line.strip()]
if not lines:
return type(exc).__name__
return "\n".join(lines[:max_lines])
__all__ = [
"FailureCategory",
"InvestigationOutcome",
"InvestigationStatus",
"classify_investigation_failure",
"failure_detail_from_exception",
"normalize_investigation_target",
"truncate_failure_detail",
"user_facing_error_message",
]