diff --git a/strix/skills/__init__.py b/strix/skills/__init__.py index 8a9a5acd..d675514c 100644 --- a/strix/skills/__init__.py +++ b/strix/skills/__init__.py @@ -8,7 +8,6 @@ from typing import TypeGuard import yaml -from strix.telemetry import posthog, scarf from strix.utils.resource_paths import get_strix_resource_path @@ -241,16 +240,22 @@ def validate_requested_skills(skill_list: list[str], max_skills: int = 5) -> str return None +_LOADED_SKILLS: set[str] = set() +_LOADED_SKILLS_LOCK = threading.Lock() + + def _track_skill_loaded(skill_name: str, file_path: Path) -> None: builtin = get_strix_resource_path("skills") if not file_path.is_relative_to(builtin): skill_name = "custom" + with _LOADED_SKILLS_LOCK: + _LOADED_SKILLS.add(skill_name) - def _send() -> None: - posthog.skill_loaded(skill_name) - scarf.skill_loaded(skill_name) - threading.Thread(target=_send, daemon=True).start() +def get_loaded_skill_names() -> list[str]: + """Distinct skills loaded so far in this process (custom skills collapse to ``"custom"``).""" + with _LOADED_SKILLS_LOCK: + return sorted(_LOADED_SKILLS) def _candidate_skill_files(skill_name: str) -> list[Path]: diff --git a/strix/telemetry/README.md b/strix/telemetry/README.md index e774ab8f..023eb932 100644 --- a/strix/telemetry/README.md +++ b/strix/telemetry/README.md @@ -16,7 +16,7 @@ We collect only very **basic** usage data including: **System Context:** OS type, architecture, Strix version\ **Scan Context:** Scan mode (quick/standard/deep), scan type (whitebox/blackbox)\ **Model Usage:** Which LLM model is being used and whether it runs via an API key or a model subscription (not prompts or responses)\ -**Feature Usage:** Which built-in skills are loaded\ +**Feature Usage:** Which built-in skills were used during a scan (reported once, at scan end)\ **Aggregate Metrics:** Vulnerability counts by severity and weakness category (CWE) ### What We **Never** Collect diff --git a/strix/telemetry/posthog.py b/strix/telemetry/posthog.py index 756f163c..a85a843b 100644 --- a/strix/telemetry/posthog.py +++ b/strix/telemetry/posthog.py @@ -4,10 +4,12 @@ from typing import TYPE_CHECKING, Any import requests from strix.config import load_settings +from strix.skills import get_loaded_skill_names from strix.telemetry._common import ( SEND_TIMEOUT, SESSION_ID, base_props, + get_version, is_first_run, ) @@ -35,7 +37,12 @@ def _send(event: str, properties: dict[str, Any]) -> bool: "api_key": _POSTHOG_PUBLIC_API_KEY, "event": event, "distinct_id": SESSION_ID, - "properties": properties, + "properties": { + **properties, + "$lib": "strix-cli", + "$lib_version": get_version(), + "$process_person_profile": False, + }, } with requests.post(f"{_POSTHOG_HOST}/capture/", json=payload, timeout=SEND_TIMEOUT): pass @@ -82,16 +89,6 @@ def finding(severity: str, cwe: str | None = None, is_cve: bool = False) -> None ) -def skill_loaded(skill_name: str) -> None: - _send( - "skill_loaded", - { - **base_props(), - "skill": skill_name, - }, - ) - - def end(report_state: "ReportState", exit_reason: str = "completed") -> None: if report_state.posthog_scan_ended_sent: return @@ -130,6 +127,7 @@ def end(report_state: "ReportState", exit_reason: str = "completed") -> None: "vulnerabilities_total": len(report_state.vulnerability_reports), **{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()}, **llm_props, + "skills": get_loaded_skill_names(), }, ) diff --git a/strix/telemetry/scarf.py b/strix/telemetry/scarf.py index 22767424..fbaea998 100644 --- a/strix/telemetry/scarf.py +++ b/strix/telemetry/scarf.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any import requests from strix.config import load_settings +from strix.skills import get_loaded_skill_names from strix.telemetry._common import ( SEND_TIMEOUT, SESSION_ID, @@ -90,17 +91,6 @@ def finding(severity: str, cwe: str | None = None, is_cve: bool = False) -> None ) -def skill_loaded(skill_name: str) -> None: - _send( - "skill_loaded", - { - **base_props(), - "session": SESSION_ID, - "skill": skill_name, - }, - ) - - def end(report_state: ReportState, exit_reason: str = "completed") -> None: if report_state.scarf_scan_ended_sent: return @@ -140,6 +130,7 @@ def end(report_state: ReportState, exit_reason: str = "completed") -> None: "vulnerabilities_total": len(report_state.vulnerability_reports), **{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()}, **llm_props, + "skills": ",".join(get_loaded_skill_names()), }, )