167 lines
6.0 KiB
Python
167 lines
6.0 KiB
Python
#!/usr/bin/env -S uv run
|
|
# /// script
|
|
# requires-python = ">=3.12"
|
|
# dependencies = [
|
|
# "pandas==2.2.3",
|
|
# "tabulate==0.9.0",
|
|
# ]
|
|
# ///
|
|
|
|
"""
|
|
Analyze Aptabase analytics export for Vibe desktop app.
|
|
|
|
Events tracked in the app:
|
|
|
|
Rust side (desktop/src-tauri/src/analytics.rs):
|
|
- app_started : fired on every app launch (main.rs)
|
|
- cli_started : fired when CLI mode is used (cli.rs)
|
|
- sona_spawn_failed : fired when sona child process fails to spawn (cmd/mod.rs)
|
|
props: { error_message: string }
|
|
|
|
TypeScript side (desktop/src/lib/analytics.ts → invokes track_analytics_event):
|
|
- transcribe_started : fired when transcription begins (home + batch)
|
|
props: { source: "home"|"batch" }
|
|
- transcribe_succeeded : fired on successful transcription
|
|
props: { source, duration_seconds, segments_count }
|
|
- transcribe_failed : fired on transcription failure
|
|
props: { source, error: string }
|
|
|
|
CSV exported via scripts/export_analytics.py from self-hosted Aptabase.
|
|
|
|
Codebase layout (for agents):
|
|
- desktop/src-tauri/src/ : Rust backend (sona spawn, model load, analytics)
|
|
- desktop/src/ : TypeScript frontend (transcribe UI, analytics events)
|
|
- sona/ : Go sona server source (whisper bindings, HTTP API, audio processing)
|
|
- plans/ : analysis reports and fix plans
|
|
"""
|
|
|
|
import json
|
|
import pandas as pd
|
|
|
|
CSV = "scripts/analytics_export.csv"
|
|
|
|
df = pd.read_csv(CSV)
|
|
|
|
# Parse JSON prop columns
|
|
def safe_json(val):
|
|
if pd.isna(val):
|
|
return {}
|
|
try:
|
|
return json.loads(val)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return {}
|
|
|
|
df["_str_props"] = df["string_props"].apply(safe_json)
|
|
df["_num_props"] = df["numeric_props"].apply(safe_json)
|
|
|
|
print("=== Shape ===")
|
|
print(f" rows: {len(df):,} cols: {df.shape[1]}")
|
|
print()
|
|
|
|
print("=== Event distribution ===")
|
|
print(df["event_name"].value_counts().to_string())
|
|
print()
|
|
|
|
print("=== OS distribution ===")
|
|
print(df["os_name"].value_counts().to_string())
|
|
print()
|
|
|
|
print("=== Events x OS crosstab ===")
|
|
ct = pd.crosstab(df["event_name"], df["os_name"])
|
|
print(ct.to_string())
|
|
print()
|
|
|
|
# ── Transcribe failure rate by OS ──
|
|
started = df[df["event_name"] == "transcribe_started"].groupby("os_name").size()
|
|
failed = df[df["event_name"] == "transcribe_failed"].groupby("os_name").size()
|
|
succeeded = df[df["event_name"] == "transcribe_succeeded"].groupby("os_name").size()
|
|
rate = pd.DataFrame({"started": started, "failed": failed, "succeeded": succeeded}).fillna(0).astype(int)
|
|
rate["fail_rate_%"] = (rate["failed"] / rate["started"] * 100).round(1)
|
|
rate = rate.sort_values("started", ascending=False)
|
|
print("=== Transcribe failure rate by OS ===")
|
|
print(rate.to_string())
|
|
print()
|
|
|
|
# ── sona_spawn_failed details ──
|
|
spawn_fail = df[df["event_name"] == "sona_spawn_failed"].copy()
|
|
print(f"=== sona_spawn_failed: {len(spawn_fail)} events ===")
|
|
if len(spawn_fail):
|
|
spawn_fail["error"] = spawn_fail["_str_props"].apply(lambda p: p.get("error_message", ""))
|
|
print()
|
|
print("--- By OS ---")
|
|
print(spawn_fail["os_name"].value_counts().to_string())
|
|
print()
|
|
print("--- By OS + OS version ---")
|
|
print(spawn_fail.groupby(["os_name", "os_version"]).size().sort_values(ascending=False).to_string())
|
|
print()
|
|
print("--- By app_version ---")
|
|
print(spawn_fail["app_version"].value_counts().to_string())
|
|
print()
|
|
print("--- Error messages (top 15) ---")
|
|
print(spawn_fail["error"].value_counts().head(15).to_string())
|
|
print()
|
|
print("--- Errors by OS (sample) ---")
|
|
for os_name, group in spawn_fail.groupby("os_name"):
|
|
print(f"\n [{os_name}] ({len(group)} events)")
|
|
for err, cnt in group["error"].value_counts().items():
|
|
print(f" ({cnt}x) {err[:200]}")
|
|
print()
|
|
|
|
# ── transcribe_failed details ──
|
|
tx_fail = df[df["event_name"] == "transcribe_failed"].copy()
|
|
print(f"=== transcribe_failed: {len(tx_fail)} events ===")
|
|
if len(tx_fail):
|
|
tx_fail["error"] = tx_fail["_str_props"].apply(lambda p: p.get("error_message", p.get("error", "")))
|
|
tx_fail["source"] = tx_fail["_str_props"].apply(lambda p: p.get("source", ""))
|
|
print()
|
|
print("--- By OS ---")
|
|
print(tx_fail["os_name"].value_counts().to_string())
|
|
print()
|
|
print("--- By source ---")
|
|
print(tx_fail["source"].value_counts().to_string())
|
|
print()
|
|
# Categorize errors
|
|
def categorize_error(err: str) -> str:
|
|
err_lower = err.lower()
|
|
if "server is busy" in err_lower:
|
|
return "server_busy"
|
|
if "no model selected" in err_lower:
|
|
return "no_model_selected"
|
|
if "failed to load model" in err_lower:
|
|
return "model_load_failed"
|
|
if "failed to send load_model" in err_lower or "connect" in err_lower:
|
|
return "sona_connection_failed"
|
|
if "sona transcribe failed" in err_lower:
|
|
return "sona_transcribe_error"
|
|
if "eof while parsing" in err_lower:
|
|
return "sona_spawn_eof"
|
|
if not err.strip():
|
|
return "empty_error"
|
|
return "other"
|
|
|
|
tx_fail["error_cat"] = tx_fail["error"].apply(categorize_error)
|
|
|
|
print("--- Error categories ---")
|
|
print(tx_fail["error_cat"].value_counts().to_string())
|
|
print()
|
|
|
|
print("--- Error categories x OS ---")
|
|
ct2 = pd.crosstab(tx_fail["error_cat"], tx_fail["os_name"])
|
|
print(ct2.to_string())
|
|
print()
|
|
|
|
print("--- Error messages (top 20, truncated) ---")
|
|
for err, cnt in tx_fail["error"].value_counts().head(20).items():
|
|
print(f" ({cnt:>4}x) {err[:180]}")
|
|
print()
|
|
print("--- Errors by OS (top errors per OS) ---")
|
|
for os_name, group in tx_fail.groupby("os_name"):
|
|
print(f"\n [{os_name}] ({len(group)} events)")
|
|
for err, cnt in group["error"].value_counts().head(5).items():
|
|
print(f" ({cnt}x) {err[:180]}")
|
|
print()
|
|
|
|
# ── App version distribution ──
|
|
print("=== App version distribution ===")
|
|
print(df["app_version"].value_counts().to_string())
|