Skip to content

Commit 66162bd

Browse files
committed
feat: gstack-community-benchmarks — compare your usage against the community
Shows your gstack usage profile (sessions, skill diversity, success rate, browse usage) and compares against community averages from the Supabase community-pulse API. Viral mechanic: power user badges, skill diversity scoring, and community comparison encourage deeper gstack adoption. Usage: gstack-community-benchmarks # full comparison gstack-community-benchmarks --local # local only gstack-community-benchmarks --json # machine-readable
1 parent 3501f5d commit 66162bd

1 file changed

Lines changed: 167 additions & 0 deletions

File tree

bin/gstack-community-benchmarks

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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

Comments
 (0)