ai-marketing-skills/telemetry/telemetry_report.py
Alfred Claw d4c8c21cb3 Add security sanitizer, opt-in telemetry, and contributor guidelines
Infrastructure:
- security/: PII sanitizer with scan/sanitize modes, pre-commit hook, configurable blocklists
- telemetry/: GStack-style opt-in usage analytics, local stats viewer, version checker
- CONTRIBUTING.md: Privacy-first contributor guidelines with anonymization rules
- VERSION: 1.0.0

README updated with Privacy & Security and Telemetry sections.
2026-03-31 08:41:35 -07:00

177 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Local stats viewer for skill usage data.
Usage:
python3 telemetry/telemetry_report.py # Full report
python3 telemetry/telemetry_report.py --json # Machine-readable output
python3 telemetry/telemetry_report.py --skill X # Filter to one skill
"""
import argparse
import json
import sys
from collections import defaultdict
from datetime import datetime, timezone, timedelta
from pathlib import Path
CONFIG_DIR = Path.home() / ".ai-marketing-skills"
CONFIG_FILE = CONFIG_DIR / "telemetry-config.json"
USAGE_LOG = CONFIG_DIR / "analytics" / "skill-usage.jsonl"
def load_entries(skill_filter: str = None) -> list:
"""Load all log entries, optionally filtered by skill."""
if not USAGE_LOG.exists():
return []
entries = []
with open(USAGE_LOG, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
if skill_filter and entry.get("skill") != skill_filter:
continue
entries.append(entry)
except json.JSONDecodeError:
continue
return entries
def load_config() -> dict:
if CONFIG_FILE.exists():
try:
with open(CONFIG_FILE, "r") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
pass
return {}
def parse_timestamp(ts: str) -> datetime:
"""Parse ISO timestamp string."""
# Handle both formats with and without timezone
try:
return datetime.fromisoformat(ts)
except ValueError:
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
def generate_report(entries: list, config: dict) -> dict:
"""Generate stats from entries."""
now = datetime.now(timezone.utc)
seven_days_ago = now - timedelta(days=7)
thirty_days_ago = now - timedelta(days=30)
total = len(entries)
last_7 = 0
last_30 = 0
skill_runs = defaultdict(int)
skill_successes = defaultdict(int)
skill_durations = defaultdict(list)
last_timestamp = None
for e in entries:
skill = e.get("skill", "unknown")
skill_runs[skill] += 1
if e.get("success"):
skill_successes[skill] += 1
duration = e.get("duration_ms")
if duration is not None:
skill_durations[skill].append(duration)
ts_str = e.get("timestamp")
if ts_str:
try:
ts = parse_timestamp(ts_str)
if ts.tzinfo is None:
ts = ts.replace(tzinfo=timezone.utc)
if ts >= seven_days_ago:
last_7 += 1
if ts >= thirty_days_ago:
last_30 += 1
if last_timestamp is None or ts > last_timestamp:
last_timestamp = ts
except (ValueError, TypeError):
pass
# Per-skill stats
per_skill = {}
for skill, count in sorted(skill_runs.items(), key=lambda x: -x[1]):
avg_dur = None
if skill_durations[skill]:
avg_dur = round(sum(skill_durations[skill]) / len(skill_durations[skill]), 1)
success_rate = round(skill_successes[skill] / count * 100, 1) if count > 0 else 0
per_skill[skill] = {
"runs": count,
"success_rate_pct": success_rate,
"avg_duration_ms": avg_dur,
}
most_used = max(skill_runs, key=skill_runs.get) if skill_runs else None
return {
"total_runs": total,
"last_7_days": last_7,
"last_30_days": last_30,
"most_used_skill": most_used,
"last_run": last_timestamp.isoformat() if last_timestamp else None,
"opted_in": config.get("opted_in", False),
"per_skill": per_skill,
}
def print_report(report: dict):
"""Pretty-print the report."""
print("=" * 50)
print(" AI Marketing Skills — Usage Report")
print("=" * 50)
print()
print(f" Total runs (all time): {report['total_runs']}")
print(f" Last 7 days: {report['last_7_days']}")
print(f" Last 30 days: {report['last_30_days']}")
print(f" Most used skill: {report['most_used_skill'] or 'N/A'}")
print(f" Last run: {report['last_run'] or 'N/A'}")
print(f" Telemetry opt-in: {'Yes' if report['opted_in'] else 'No'}")
print()
if report["per_skill"]:
print(" Per-Skill Breakdown:")
print(" " + "-" * 46)
print(f" {'Skill':<25} {'Runs':>5} {'Success':>8} {'Avg ms':>8}")
print(" " + "-" * 46)
for skill, stats in report["per_skill"].items():
avg = f"{stats['avg_duration_ms']:.0f}" if stats["avg_duration_ms"] is not None else "N/A"
print(f" {skill:<25} {stats['runs']:>5} {stats['success_rate_pct']:>7.1f}% {avg:>8}")
else:
print(" No usage data found.")
print()
def main():
parser = argparse.ArgumentParser(description="View local skill usage stats.")
parser.add_argument("--json", action="store_true", help="Output as JSON.")
parser.add_argument("--skill", help="Filter to a specific skill.")
args = parser.parse_args()
config = load_config()
entries = load_entries(skill_filter=args.skill)
if not entries and not args.json:
print("No usage data found. Run some skills first!")
print(f"Data location: {USAGE_LOG}")
return
report = generate_report(entries, config)
if args.json:
print(json.dumps(report, indent=2))
else:
print_report(report)
if __name__ == "__main__":
main()