~cytrogen/gstack

ref: cf73db5f19040218ecd50d0b81acffd40b63f056 gstack/bin/gstack-analytics -rwxr-xr-x 6.3 KiB
cf73db5f — Garry Tan feat: autoplan DX integration + README docs (v0.15.4.0) (#791) 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env bash
# gstack-analytics — personal usage dashboard from local JSONL
#
# Usage:
#   gstack-analytics          # default: last 7 days
#   gstack-analytics 7d       # last 7 days
#   gstack-analytics 30d      # last 30 days
#   gstack-analytics all      # all time
#
# Env overrides (for testing):
#   GSTACK_STATE_DIR  — override ~/.gstack state directory
set -uo pipefail

STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
JSONL_FILE="$STATE_DIR/analytics/skill-usage.jsonl"

# ─── Parse time window ───────────────────────────────────────
WINDOW="${1:-7d}"
case "$WINDOW" in
  7d)  DAYS=7;  LABEL="last 7 days" ;;
  30d) DAYS=30; LABEL="last 30 days" ;;
  all) DAYS=0;  LABEL="all time" ;;
  *)   DAYS=7;  LABEL="last 7 days" ;;
esac

# ─── Check for data ──────────────────────────────────────────
if [ ! -f "$JSONL_FILE" ]; then
  echo "gstack usage — no data yet"
  echo ""
  echo "Usage data will appear here after you use gstack skills"
  echo "with telemetry enabled (gstack-config set telemetry anonymous)."
  exit 0
fi

TOTAL_LINES="$(wc -l < "$JSONL_FILE" | tr -d ' ')"
if [ "$TOTAL_LINES" = "0" ]; then
  echo "gstack usage — no data yet"
  exit 0
fi

# ─── Filter by time window ───────────────────────────────────
if [ "$DAYS" -gt 0 ] 2>/dev/null; then
  # Calculate cutoff date
  if date -v-1d +%Y-%m-%d >/dev/null 2>&1; then
    # macOS date
    CUTOFF="$(date -v-${DAYS}d -u +%Y-%m-%dT%H:%M:%SZ)"
  else
    # GNU date
    CUTOFF="$(date -u -d "$DAYS days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "2000-01-01T00:00:00Z")"
  fi
  # Filter: skill_run events (new format) OR basic skill events (old format, no event_type)
  # Old format: {"skill":"X","ts":"Y","repo":"Z"} (no event_type field)
  # New format: {"event_type":"skill_run","skill":"X","ts":"Y",...}
  FILTERED="$(awk -F'"' -v cutoff="$CUTOFF" '
    /"ts":"/ {
      # Skip hook_fire events
      if (/"event":"hook_fire"/) next
      # Skip non-skill_run new-format events
      if (/"event_type":"/ && !/"event_type":"skill_run"/) next
      for (i=1; i<=NF; i++) {
        if ($i == "ts" && $(i+1) ~ /^:/) {
          ts = $(i+2)
          if (ts >= cutoff) { print; break }
        }
      }
    }
  ' "$JSONL_FILE")"
else
  # All time: include skill_run events + old-format basic events, exclude hook_fire
  FILTERED="$(awk '/"ts":"/ && !/"event":"hook_fire"/' "$JSONL_FILE" | grep -v '"event_type":"upgrade_' 2>/dev/null || true)"
fi

if [ -z "$FILTERED" ]; then
  echo "gstack usage ($LABEL) — no skill runs found"
  exit 0
fi

# ─── Aggregate by skill ──────────────────────────────────────
# Extract skill names and count
SKILL_COUNTS="$(echo "$FILTERED" | awk -F'"' '
  /"skill":"/ {
    for (i=1; i<=NF; i++) {
      if ($i == "skill" && $(i+1) ~ /^:/) {
        skill = $(i+2)
        counts[skill]++
        break
      }
    }
  }
  END {
    for (s in counts) print counts[s], s
  }
' | sort -rn)"

# Count outcomes
TOTAL="$(echo "$FILTERED" | wc -l | tr -d ' ')"
SUCCESS="$(echo "$FILTERED" | grep -c '"outcome":"success"' || true)"
SUCCESS="${SUCCESS:-0}"; SUCCESS="$(echo "$SUCCESS" | tr -d ' \n\r\t')"
ERRORS="$(echo "$FILTERED" | grep -c '"outcome":"error"' || true)"
ERRORS="${ERRORS:-0}"; ERRORS="$(echo "$ERRORS" | tr -d ' \n\r\t')"
# Old format events have no outcome field — count them as successful
NO_OUTCOME="$(echo "$FILTERED" | grep -vc '"outcome":' || true)"
NO_OUTCOME="${NO_OUTCOME:-0}"; NO_OUTCOME="$(echo "$NO_OUTCOME" | tr -d ' \n\r\t')"
SUCCESS=$(( SUCCESS + NO_OUTCOME ))

# Calculate success rate
if [ "$TOTAL" -gt 0 ] 2>/dev/null; then
  SUCCESS_RATE=$(( SUCCESS * 100 / TOTAL ))
else
  SUCCESS_RATE=100
fi

# ─── Calculate total duration ────────────────────────────────
TOTAL_DURATION="$(echo "$FILTERED" | awk -F'[:,]' '
  /"duration_s"/ {
    for (i=1; i<=NF; i++) {
      if ($i ~ /"duration_s"/) {
        val = $(i+1)
        gsub(/[^0-9.]/, "", val)
        if (val+0 > 0) total += val
      }
    }
  }
  END { printf "%.0f", total }
')"

# Format duration
TOTAL_DURATION="${TOTAL_DURATION:-0}"
if [ "$TOTAL_DURATION" -ge 3600 ] 2>/dev/null; then
  HOURS=$(( TOTAL_DURATION / 3600 ))
  MINS=$(( (TOTAL_DURATION % 3600) / 60 ))
  DUR_DISPLAY="${HOURS}h ${MINS}m"
elif [ "$TOTAL_DURATION" -ge 60 ] 2>/dev/null; then
  MINS=$(( TOTAL_DURATION / 60 ))
  DUR_DISPLAY="${MINS}m"
else
  DUR_DISPLAY="${TOTAL_DURATION}s"
fi

# ─── Render output ───────────────────────────────────────────
echo "gstack usage ($LABEL)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# Find max count for bar scaling
MAX_COUNT="$(echo "$SKILL_COUNTS" | head -1 | awk '{print $1}')"
BAR_WIDTH=20

echo "$SKILL_COUNTS" | while read -r COUNT SKILL; do
  # Scale bar
  if [ "$MAX_COUNT" -gt 0 ] 2>/dev/null; then
    BAR_LEN=$(( COUNT * BAR_WIDTH / MAX_COUNT ))
  else
    BAR_LEN=1
  fi
  [ "$BAR_LEN" -lt 1 ] && BAR_LEN=1

  # Build bar
  BAR=""
  i=0
  while [ "$i" -lt "$BAR_LEN" ]; do
    BAR="${BAR}█"
    i=$(( i + 1 ))
  done

  # Calculate avg duration for this skill
  AVG_DUR="$(echo "$FILTERED" | awk -v skill="$SKILL" '
    index($0, "\"skill\":\"" skill "\"") > 0 {
      # Extract duration_s value using split on "duration_s":
      n = split($0, parts, "\"duration_s\":")
      if (n >= 2) {
        # parts[2] starts with the value, e.g. "142,"
        gsub(/[^0-9.].*/, "", parts[2])
        if (parts[2]+0 > 0) { total += parts[2]; count++ }
      }
    }
    END { if (count > 0) printf "%.0f", total/count; else print "0" }
  ')"

  # Format avg duration
  if [ "$AVG_DUR" -ge 60 ] 2>/dev/null; then
    AVG_DISPLAY="$(( AVG_DUR / 60 ))m"
  else
    AVG_DISPLAY="${AVG_DUR}s"
  fi

  printf "  /%-20s %s  %d runs  (avg %s)\n" "$SKILL" "$BAR" "$COUNT" "$AVG_DISPLAY"
done

echo ""
echo "Success rate: ${SUCCESS_RATE}% | Errors: ${ERRORS} | Total time: ${DUR_DISPLAY}"
echo "Events: ${TOTAL} skill runs"