mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
Bare mktemp, mktemp -t and literal /tmp templates in the learnings,
question and preference logs (plus jsonl-merge, distill, the two
dashboards, /ship's PR body and /document-release's run dir) now use
"${TMPDIR:-/tmp}/<name>.XXXXXX", the form the INV-3 lint accepts. When
the temp file cannot be created, the learnings, question and preference
logs now say the entry was not recorded and how to fix TMPDIR instead of
exiting with only mktemp's own error. Three tests that pinned the bare
PR_BODY_FILE=$(mktemp) and one that pinned the /tmp run dir migrate.
GNU and BSD mktemp already honor TMPDIR without a template, so the
macOS-sandbox drop in #2952 is unverified here; the template makes the
temp location explicit on every mktemp.
Co-authored-by: Davon Johnson <d.johnson@dayofshow.net>
142 lines
6.4 KiB
Bash
Executable File
142 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-community-dashboard — community usage stats from Supabase
|
|
#
|
|
# Calls the community-pulse edge function for aggregated stats:
|
|
# skill popularity, crash clusters, version distribution, retention.
|
|
#
|
|
# Env overrides (for testing):
|
|
# GSTACK_DIR — override auto-detected gstack root
|
|
# GSTACK_SUPABASE_URL — override Supabase project URL
|
|
# GSTACK_SUPABASE_ANON_KEY — override Supabase anon key
|
|
set -uo pipefail
|
|
|
|
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
|
|
# Egress receipt helpers (_receipted_curl): fail-open for read-only stats.
|
|
. "$GSTACK_DIR/bin/gstack-egress-lib.sh"
|
|
|
|
# Source Supabase config if not overridden by env
|
|
if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
|
. "$GSTACK_DIR/supabase/config.sh"
|
|
fi
|
|
SUPABASE_URL="${GSTACK_SUPABASE_URL:-}"
|
|
ANON_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
|
|
|
|
if [ -z "$SUPABASE_URL" ] || [ -z "$ANON_KEY" ]; then
|
|
echo "gstack community dashboard"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
echo "Supabase not configured yet. The community dashboard will be"
|
|
echo "available once the gstack Supabase project is set up."
|
|
echo ""
|
|
echo "For local analytics, run: gstack-analytics"
|
|
exit 0
|
|
fi
|
|
|
|
# ─── Fetch aggregated stats from edge function ────────────────
|
|
# HTTP status captured (#1947): a backend failure must read as "unknown",
|
|
# never as a healthy "Weekly active installs: 0".
|
|
TMPBODY="$(mktemp "${TMPDIR:-/tmp}/gstack-community.XXXXXX")"
|
|
trap 'rm -f "$TMPBODY"' EXIT
|
|
SUPA_HOST="${SUPABASE_URL#*://}"; SUPA_HOST="${SUPA_HOST%%/*}"
|
|
HTTP_CODE="$(_receipted_curl open community-dashboard "$SUPA_HOST" community-pulse-fetch "user-invoked dashboard" --no-payload \
|
|
curl -s --max-time 15 -w '%{http_code}' -o "$TMPBODY" \
|
|
"${SUPABASE_URL}/functions/v1/community-pulse" \
|
|
-H "apikey: ${ANON_KEY}" || true)"
|
|
# curl prints its own 000 before a non-zero exit — a `|| echo` here would
|
|
# double it to "000000" in user-facing output. Normalize to the last 3 chars.
|
|
HTTP_CODE="$(printf '%s' "$HTTP_CODE" | tr -d '[:space:]' | tail -c 3)"
|
|
[ -n "$HTTP_CODE" ] || HTTP_CODE="000"
|
|
DATA="$(cat "$TMPBODY" 2>/dev/null || echo "")"
|
|
|
|
echo "gstack community dashboard"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
|
|
if [ "$HTTP_CODE" != "200" ] || [ -z "$DATA" ] || ! printf '%s' "$DATA" | grep -q '"weekly_active"'; then
|
|
echo "Community stats: unknown — backend error (HTTP ${HTTP_CODE})"
|
|
echo ""
|
|
echo "For local analytics: gstack-analytics"
|
|
exit 0
|
|
fi
|
|
|
|
# ─── Weekly active installs ──────────────────────────────────
|
|
WEEKLY="$(echo "$DATA" | grep -o '"weekly_active":[0-9]*' | grep -o '[0-9]*' || echo "0")"
|
|
CHANGE="$(echo "$DATA" | grep -o '"change_pct":[0-9-]*' | grep -o '[0-9-]*' || echo "0")"
|
|
|
|
echo "Weekly active installs: ${WEEKLY}"
|
|
# Marker check: jq when available (whitespace/reserialization-proof); the
|
|
# grep fallback tolerates optional whitespace around the colon.
|
|
_STALE="false"
|
|
if command -v jq >/dev/null 2>&1; then
|
|
_MARKER="$(printf '%s' "$DATA" | jq -r '.status // empty' 2>/dev/null)"
|
|
_STALE="$(printf '%s' "$DATA" | jq -r '.stale // false' 2>/dev/null)"
|
|
else
|
|
_MARKER="$(printf '%s' "$DATA" | grep -Eq '"status"[[:space:]]*:[[:space:]]*"ok"' && echo ok || true)"
|
|
fi
|
|
if [ "$_MARKER" != "ok" ]; then
|
|
echo " (unverified — legacy backend response; deploy the latest community-pulse for verified figures)"
|
|
elif [ "$_STALE" = "true" ]; then
|
|
# Backend serves its last good snapshot when recompute fails — real but
|
|
# frozen figures must not read as current (matches security-dashboard).
|
|
echo " (stale snapshot — backend recompute failing; figures may be out of date)"
|
|
fi
|
|
if [ "$CHANGE" -gt 0 ] 2>/dev/null; then
|
|
echo " Change: +${CHANGE}%"
|
|
elif [ "$CHANGE" -lt 0 ] 2>/dev/null; then
|
|
echo " Change: ${CHANGE}%"
|
|
fi
|
|
echo ""
|
|
|
|
# ─── Skill popularity (top 10) ───────────────────────────────
|
|
echo "Top skills (last 7 days)"
|
|
echo "────────────────────────"
|
|
|
|
# Parse top_skills array from JSON
|
|
SKILLS="$(echo "$DATA" | grep -o '"top_skills":\[[^]]*\]' || echo "")"
|
|
if [ -n "$SKILLS" ] && [ "$SKILLS" != '"top_skills":[]' ]; then
|
|
# Parse each object — handle any key order (JSONB doesn't preserve order)
|
|
echo "$SKILLS" | grep -o '{[^}]*}' | while read -r OBJ; do
|
|
SKILL="$(echo "$OBJ" | grep -o '"skill":"[^"]*"' | awk -F'"' '{print $4}')"
|
|
COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')"
|
|
[ -n "$SKILL" ] && [ -n "$COUNT" ] && printf " /%-20s %s runs\n" "$SKILL" "$COUNT"
|
|
done
|
|
else
|
|
echo " No data yet"
|
|
fi
|
|
echo ""
|
|
|
|
# ─── Crash clusters ──────────────────────────────────────────
|
|
echo "Top crash clusters"
|
|
echo "──────────────────"
|
|
|
|
CRASHES="$(echo "$DATA" | grep -o '"crashes":\[[^]]*\]' || echo "")"
|
|
if [ -n "$CRASHES" ] && [ "$CRASHES" != '"crashes":[]' ]; then
|
|
echo "$CRASHES" | grep -o '{[^}]*}' | head -5 | while read -r OBJ; do
|
|
ERR="$(echo "$OBJ" | grep -o '"error_class":"[^"]*"' | awk -F'"' '{print $4}')"
|
|
C="$(echo "$OBJ" | grep -o '"total_occurrences":[0-9]*' | grep -o '[0-9]*')"
|
|
[ -n "$ERR" ] && printf " %-30s %s occurrences\n" "$ERR" "${C:-?}"
|
|
done
|
|
else
|
|
echo " No crashes reported"
|
|
fi
|
|
echo ""
|
|
|
|
# ─── Version distribution ────────────────────────────────────
|
|
echo "Version distribution (last 7 days)"
|
|
echo "───────────────────────────────────"
|
|
|
|
VERSIONS="$(echo "$DATA" | grep -o '"versions":\[[^]]*\]' || echo "")"
|
|
if [ -n "$VERSIONS" ] && [ "$VERSIONS" != '"versions":[]' ]; then
|
|
echo "$VERSIONS" | grep -o '{[^}]*}' | head -5 | while read -r OBJ; do
|
|
VER="$(echo "$OBJ" | grep -o '"version":"[^"]*"' | awk -F'"' '{print $4}')"
|
|
COUNT="$(echo "$OBJ" | grep -o '"count":[0-9]*' | grep -o '[0-9]*')"
|
|
[ -n "$VER" ] && [ -n "$COUNT" ] && printf " v%-15s %s events\n" "$VER" "$COUNT"
|
|
done
|
|
else
|
|
echo " No data yet"
|
|
fi
|
|
|
|
echo ""
|
|
echo "For local analytics: gstack-analytics"
|