|
| 1 | +#!/usr/bin/env bash |
| 2 | +# gstack-community-benchmarks — compare your metrics against the community |
| 3 | +# |
| 4 | +# Reads local analytics (skill-usage.jsonl) and computes your metrics, |
| 5 | +# then compares against community averages from the Supabase API. |
| 6 | +# If community API is unavailable, shows local-only metrics. |
| 7 | +# |
| 8 | +# Usage: |
| 9 | +# gstack-community-benchmarks # full comparison |
| 10 | +# gstack-community-benchmarks --local # local metrics only |
| 11 | +# gstack-community-benchmarks --json # machine-readable |
| 12 | +# |
| 13 | +# Viral mechanic: "Your /review catch rate is top 20% of gstack users" |
| 14 | +set -uo pipefail |
| 15 | + |
| 16 | +GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" |
| 17 | +STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}" |
| 18 | +ANALYTICS_DIR="$STATE_DIR/analytics" |
| 19 | +JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl" |
| 20 | +LOCAL_ONLY="" |
| 21 | +JSON_MODE="" |
| 22 | + |
| 23 | +for arg in "$@"; do |
| 24 | + case "$arg" in |
| 25 | + --local) LOCAL_ONLY=1 ;; |
| 26 | + --json) JSON_MODE=1 ;; |
| 27 | + esac |
| 28 | +done |
| 29 | + |
| 30 | +python3 - "$JSONL_FILE" "$LOCAL_ONLY" "$JSON_MODE" "$GSTACK_DIR" << 'PYEOF' |
| 31 | +import json, sys, os |
| 32 | +from collections import defaultdict, Counter |
| 33 | +from datetime import datetime, timedelta |
| 34 | +
|
| 35 | +jsonl_file = sys.argv[1] |
| 36 | +local_only = sys.argv[2] == '1' |
| 37 | +json_mode = sys.argv[3] == '1' |
| 38 | +gstack_dir = sys.argv[4] |
| 39 | +
|
| 40 | +# ─── Read local analytics ──────────────────────────────────── |
| 41 | +events = [] |
| 42 | +if os.path.exists(jsonl_file): |
| 43 | + for line in open(jsonl_file): |
| 44 | + try: |
| 45 | + events.append(json.loads(line.strip())) |
| 46 | + except: |
| 47 | + continue |
| 48 | +
|
| 49 | +# Filter to last 30 days |
| 50 | +cutoff = (datetime.utcnow() - timedelta(days=30)).isoformat() |
| 51 | +recent = [e for e in events if e.get('ts', '') >= cutoff] |
| 52 | +
|
| 53 | +if not recent: |
| 54 | + if json_mode: |
| 55 | + print(json.dumps({"error": "no_data", "message": "No usage data in last 30 days"})) |
| 56 | + else: |
| 57 | + print("No usage data in the last 30 days.") |
| 58 | + print("Use gstack skills to start building your profile.") |
| 59 | + sys.exit(0) |
| 60 | +
|
| 61 | +# ─── Compute local metrics ─────────────────────────────────── |
| 62 | +skill_counts = Counter(e.get('skill', 'unknown') for e in recent) |
| 63 | +total_sessions = len(recent) |
| 64 | +total_duration = sum(e.get('duration_s', 0) or 0 for e in recent if isinstance(e.get('duration_s'), (int, float))) |
| 65 | +avg_duration = round(total_duration / max(total_sessions, 1)) |
| 66 | +success_rate = round(sum(1 for e in recent if e.get('outcome') == 'success') / max(total_sessions, 1) * 100) |
| 67 | +browse_pct = round(sum(1 for e in recent if e.get('used_browse')) / max(total_sessions, 1) * 100) |
| 68 | +unique_skills = len(skill_counts) |
| 69 | +top_skills = skill_counts.most_common(5) |
| 70 | +
|
| 71 | +# Sessions per day |
| 72 | +days_active = len(set(e.get('ts', '')[:10] for e in recent if e.get('ts'))) |
| 73 | +sessions_per_day = round(total_sessions / max(days_active, 1), 1) |
| 74 | +
|
| 75 | +# Skill diversity score (0-100): how many of the available skills do you use? |
| 76 | +available_skills = len([d for d in os.listdir(gstack_dir) |
| 77 | + if os.path.isdir(os.path.join(gstack_dir, d)) |
| 78 | + and os.path.exists(os.path.join(gstack_dir, d, 'SKILL.md.tmpl')) |
| 79 | + and not d.startswith('.')]) |
| 80 | +diversity = round(min(unique_skills / max(available_skills, 1) * 100, 100)) |
| 81 | +
|
| 82 | +local_metrics = { |
| 83 | + 'total_sessions': total_sessions, |
| 84 | + 'days_active': days_active, |
| 85 | + 'sessions_per_day': sessions_per_day, |
| 86 | + 'avg_duration_s': avg_duration, |
| 87 | + 'success_rate': success_rate, |
| 88 | + 'browse_usage_pct': browse_pct, |
| 89 | + 'unique_skills': unique_skills, |
| 90 | + 'available_skills': available_skills, |
| 91 | + 'skill_diversity': diversity, |
| 92 | + 'top_skills': [{'skill': s, 'count': c} for s, c in top_skills], |
| 93 | +} |
| 94 | +
|
| 95 | +# ─── Fetch community benchmarks (if available) ────────────── |
| 96 | +community = None |
| 97 | +if not local_only: |
| 98 | + try: |
| 99 | + config_file = os.path.join(gstack_dir, 'supabase', 'config.sh') |
| 100 | + endpoint = os.environ.get('GSTACK_SUPABASE_URL', '') |
| 101 | + anon_key = os.environ.get('GSTACK_SUPABASE_ANON_KEY', '') |
| 102 | +
|
| 103 | + if not endpoint and os.path.exists(config_file): |
| 104 | + for line in open(config_file): |
| 105 | + if 'SUPABASE_URL' in line and '=' in line: |
| 106 | + endpoint = line.split('=', 1)[1].strip().strip('"').strip("'") |
| 107 | + elif 'ANON_KEY' in line and '=' in line: |
| 108 | + anon_key = line.split('=', 1)[1].strip().strip('"').strip("'") |
| 109 | +
|
| 110 | + if endpoint and anon_key: |
| 111 | + import urllib.request |
| 112 | + req = urllib.request.Request( |
| 113 | + f"{endpoint}/functions/v1/community-pulse", |
| 114 | + headers={"Authorization": f"Bearer {anon_key}"} |
| 115 | + ) |
| 116 | + resp = urllib.request.urlopen(req, timeout=5) |
| 117 | + community = json.loads(resp.read()) |
| 118 | + except: |
| 119 | + pass # community data unavailable — show local only |
| 120 | +
|
| 121 | +# ─── Output ────────────────────────────────────────────────── |
| 122 | +output = { |
| 123 | + 'period': 'last_30_days', |
| 124 | + 'local': local_metrics, |
| 125 | + 'community': community, |
| 126 | +} |
| 127 | +
|
| 128 | +if json_mode: |
| 129 | + print(json.dumps(output, indent=2)) |
| 130 | + sys.exit(0) |
| 131 | +
|
| 132 | +# Human-readable |
| 133 | +print("YOUR GSTACK PROFILE (last 30 days)") |
| 134 | +print("═" * 50) |
| 135 | +print(f" Sessions: {total_sessions}") |
| 136 | +print(f" Days active: {days_active}") |
| 137 | +print(f" Sessions/day: {sessions_per_day}") |
| 138 | +print(f" Avg duration: {avg_duration}s") |
| 139 | +print(f" Success rate: {success_rate}%") |
| 140 | +print(f" Browse usage: {browse_pct}%") |
| 141 | +print(f" Skill diversity: {diversity}% ({unique_skills}/{available_skills} skills used)") |
| 142 | +print() |
| 143 | +
|
| 144 | +print("TOP SKILLS:") |
| 145 | +for s, c in top_skills: |
| 146 | + bar = "█" * min(c, 30) |
| 147 | + print(f" /%-18s %3d {bar}" % (s, c)) |
| 148 | +print() |
| 149 | +
|
| 150 | +if community: |
| 151 | + wa = community.get('weekly_active', 0) |
| 152 | + change = community.get('change_pct', 0) |
| 153 | + print("COMMUNITY:") |
| 154 | + print(f" Weekly active users: {wa} ({'+' if change >= 0 else ''}{change}%)") |
| 155 | + print() |
| 156 | + if sessions_per_day > 5: |
| 157 | + print(" 🔥 You're a power user — top tier session frequency") |
| 158 | + elif sessions_per_day > 2: |
| 159 | + print(" ⚡ Active user — above average engagement") |
| 160 | + if diversity >= 50: |
| 161 | + print(" 🎯 High skill diversity — exploring the full toolkit") |
| 162 | + elif diversity < 20: |
| 163 | + print(" 💡 Tip: try /cso (security), /retro (retrospective), /qa (testing)") |
| 164 | +else: |
| 165 | + print("COMMUNITY: Not available (telemetry not configured)") |
| 166 | + print(" Enable with: gstack-config set telemetry basic") |
| 167 | +PYEOF |
0 commit comments