mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
8500136d15
* fix: telemetry source tagging + duration guards Add --source, --error-message, --failed-step flags to gstack-telemetry-log. Source tagging (live vs test via GSTACK_TELEMETRY_SOURCE env) prevents E2E tests from polluting production data. Duration guards cap unreasonable values (>24h or negative → null). Partial cherry-pick from garrytan/community-mode — non-breaking parts only. Skips install_fingerprint rename (needs schema migration). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: remove trigger guard + proactive opt-out prompt Remove "MANUAL TRIGGER ONLY" injection from all skill descriptions. This frees 59 chars per skill from the 1024-char Codex description budget and lets skills auto-fire based on semantic matching. Merge auto-fire control into the existing `proactive` setting — when false, Claude won't auto-invoke skills or suggest them. Users are prompted once about this preference (chains after the telemetry prompt, fires on second skill run). Also trims the root gstack description by removing the skill catalog (already in the body), saving ~500 chars. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.11.16.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
194 lines
7.8 KiB
Bash
Executable File
194 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-telemetry-log — append a telemetry event to local JSONL
|
|
#
|
|
# Data flow:
|
|
# preamble (start) ──▶ .pending marker
|
|
# preamble (epilogue) ──▶ gstack-telemetry-log ──▶ skill-usage.jsonl
|
|
# └──▶ gstack-telemetry-sync (bg)
|
|
#
|
|
# Usage:
|
|
# gstack-telemetry-log --skill qa --duration 142 --outcome success \
|
|
# --used-browse true --session-id "12345-1710756600"
|
|
#
|
|
# Env overrides (for testing):
|
|
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
|
# GSTACK_DIR — override auto-detected gstack root
|
|
#
|
|
# NOTE: Uses set -uo pipefail (no -e) — telemetry must never exit non-zero
|
|
set -uo pipefail
|
|
|
|
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
|
ANALYTICS_DIR="$STATE_DIR/analytics"
|
|
JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl"
|
|
PENDING_DIR="$ANALYTICS_DIR" # .pending-* files live here
|
|
CONFIG_CMD="$GSTACK_DIR/bin/gstack-config"
|
|
VERSION_FILE="$GSTACK_DIR/VERSION"
|
|
|
|
# ─── Parse flags ─────────────────────────────────────────────
|
|
SKILL=""
|
|
DURATION=""
|
|
OUTCOME="unknown"
|
|
USED_BROWSE="false"
|
|
SESSION_ID=""
|
|
ERROR_CLASS=""
|
|
ERROR_MESSAGE=""
|
|
FAILED_STEP=""
|
|
EVENT_TYPE="skill_run"
|
|
SOURCE=""
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--skill) SKILL="$2"; shift 2 ;;
|
|
--duration) DURATION="$2"; shift 2 ;;
|
|
--outcome) OUTCOME="$2"; shift 2 ;;
|
|
--used-browse) USED_BROWSE="$2"; shift 2 ;;
|
|
--session-id) SESSION_ID="$2"; shift 2 ;;
|
|
--error-class) ERROR_CLASS="$2"; shift 2 ;;
|
|
--error-message) ERROR_MESSAGE="$2"; shift 2 ;;
|
|
--failed-step) FAILED_STEP="$2"; shift 2 ;;
|
|
--event-type) EVENT_TYPE="$2"; shift 2 ;;
|
|
--source) SOURCE="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
# Source: flag > env > default 'live'
|
|
SOURCE="${SOURCE:-${GSTACK_TELEMETRY_SOURCE:-live}}"
|
|
|
|
# ─── Read telemetry tier ─────────────────────────────────────
|
|
TIER="$("$CONFIG_CMD" get telemetry 2>/dev/null || true)"
|
|
TIER="${TIER:-off}"
|
|
|
|
# Validate tier
|
|
case "$TIER" in
|
|
off|anonymous|community) ;;
|
|
*) TIER="off" ;; # invalid value → default to off
|
|
esac
|
|
|
|
if [ "$TIER" = "off" ]; then
|
|
# Still clear pending markers for this session even if telemetry is off
|
|
[ -n "$SESSION_ID" ] && rm -f "$PENDING_DIR/.pending-$SESSION_ID" 2>/dev/null || true
|
|
exit 0
|
|
fi
|
|
|
|
# ─── Finalize stale .pending markers ────────────────────────
|
|
# Each session gets its own .pending-$SESSION_ID file to avoid races
|
|
# between concurrent sessions. Finalize any that don't match our session.
|
|
for PFILE in "$PENDING_DIR"/.pending-*; do
|
|
[ -f "$PFILE" ] || continue
|
|
# Skip our own session's marker (it's still in-flight)
|
|
PFILE_BASE="$(basename "$PFILE")"
|
|
PFILE_SID="${PFILE_BASE#.pending-}"
|
|
[ "$PFILE_SID" = "$SESSION_ID" ] && continue
|
|
|
|
PENDING_DATA="$(cat "$PFILE" 2>/dev/null || true)"
|
|
rm -f "$PFILE" 2>/dev/null || true
|
|
if [ -n "$PENDING_DATA" ]; then
|
|
# Extract fields from pending marker using grep -o + awk
|
|
P_SKILL="$(echo "$PENDING_DATA" | grep -o '"skill":"[^"]*"' | head -1 | awk -F'"' '{print $4}')"
|
|
P_TS="$(echo "$PENDING_DATA" | grep -o '"ts":"[^"]*"' | head -1 | awk -F'"' '{print $4}')"
|
|
P_SID="$(echo "$PENDING_DATA" | grep -o '"session_id":"[^"]*"' | head -1 | awk -F'"' '{print $4}')"
|
|
P_VER="$(echo "$PENDING_DATA" | grep -o '"gstack_version":"[^"]*"' | head -1 | awk -F'"' '{print $4}')"
|
|
P_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
P_ARCH="$(uname -m)"
|
|
|
|
# Write the stale event as outcome: unknown
|
|
mkdir -p "$ANALYTICS_DIR"
|
|
printf '{"v":1,"ts":"%s","event_type":"skill_run","skill":"%s","session_id":"%s","gstack_version":"%s","os":"%s","arch":"%s","duration_s":null,"outcome":"unknown","error_class":null,"used_browse":false,"sessions":1}\n' \
|
|
"$P_TS" "$P_SKILL" "$P_SID" "$P_VER" "$P_OS" "$P_ARCH" >> "$JSONL_FILE" 2>/dev/null || true
|
|
fi
|
|
done
|
|
|
|
# Clear our own session's pending marker (we're about to log the real event)
|
|
[ -n "$SESSION_ID" ] && rm -f "$PENDING_DIR/.pending-$SESSION_ID" 2>/dev/null || true
|
|
|
|
# ─── Collect metadata ────────────────────────────────────────
|
|
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%S 2>/dev/null || echo "")"
|
|
GSTACK_VERSION="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]' || echo "unknown")"
|
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
ARCH="$(uname -m)"
|
|
SESSIONS="1"
|
|
if [ -d "$STATE_DIR/sessions" ]; then
|
|
_SC="$(find "$STATE_DIR/sessions" -mmin -120 -type f 2>/dev/null | wc -l | tr -d ' \n\r\t')"
|
|
[ -n "$_SC" ] && [ "$_SC" -gt 0 ] 2>/dev/null && SESSIONS="$_SC"
|
|
fi
|
|
|
|
# Generate installation_id for community tier
|
|
# Uses a random UUID stored locally — not derived from hostname/user so it
|
|
# can't be guessed or correlated by someone who knows your machine identity.
|
|
INSTALL_ID=""
|
|
if [ "$TIER" = "community" ]; then
|
|
ID_FILE="$HOME/.gstack/installation-id"
|
|
if [ -f "$ID_FILE" ]; then
|
|
INSTALL_ID="$(cat "$ID_FILE" 2>/dev/null)"
|
|
fi
|
|
if [ -z "$INSTALL_ID" ]; then
|
|
# Generate a random UUID v4
|
|
if command -v uuidgen >/dev/null 2>&1; then
|
|
INSTALL_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
|
|
elif [ -r /proc/sys/kernel/random/uuid ]; then
|
|
INSTALL_ID="$(cat /proc/sys/kernel/random/uuid)"
|
|
else
|
|
# Fallback: random hex from /dev/urandom
|
|
INSTALL_ID="$(od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n')"
|
|
fi
|
|
if [ -n "$INSTALL_ID" ]; then
|
|
mkdir -p "$(dirname "$ID_FILE")" 2>/dev/null
|
|
printf '%s' "$INSTALL_ID" > "$ID_FILE" 2>/dev/null
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Local-only fields (never sent remotely)
|
|
REPO_SLUG=""
|
|
BRANCH=""
|
|
if command -v git >/dev/null 2>&1; then
|
|
REPO_SLUG="$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-' 2>/dev/null || true)"
|
|
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
fi
|
|
|
|
# ─── Construct and append JSON ───────────────────────────────
|
|
mkdir -p "$ANALYTICS_DIR"
|
|
|
|
# Escape null fields
|
|
ERR_FIELD="null"
|
|
[ -n "$ERROR_CLASS" ] && ERR_FIELD="\"$ERROR_CLASS\""
|
|
|
|
ERR_MSG_FIELD="null"
|
|
[ -n "$ERROR_MESSAGE" ] && ERR_MSG_FIELD="\"$(echo "$ERROR_MESSAGE" | head -c 200 | sed 's/"/\\"/g')\""
|
|
|
|
STEP_FIELD="null"
|
|
[ -n "$FAILED_STEP" ] && STEP_FIELD="\"$(echo "$FAILED_STEP" | head -c 30)\""
|
|
|
|
# Cap unreasonable durations
|
|
if [ -n "$DURATION" ] && [ "$DURATION" -gt 86400 ] 2>/dev/null; then
|
|
DURATION="" # null if > 24h
|
|
fi
|
|
if [ -n "$DURATION" ] && [ "$DURATION" -lt 0 ] 2>/dev/null; then
|
|
DURATION="" # null if negative
|
|
fi
|
|
|
|
DUR_FIELD="null"
|
|
[ -n "$DURATION" ] && DUR_FIELD="$DURATION"
|
|
|
|
INSTALL_FIELD="null"
|
|
[ -n "$INSTALL_ID" ] && INSTALL_FIELD="\"$INSTALL_ID\""
|
|
|
|
BROWSE_BOOL="false"
|
|
[ "$USED_BROWSE" = "true" ] && BROWSE_BOOL="true"
|
|
|
|
printf '{"v":1,"ts":"%s","event_type":"%s","skill":"%s","session_id":"%s","gstack_version":"%s","os":"%s","arch":"%s","duration_s":%s,"outcome":"%s","error_class":%s,"error_message":%s,"failed_step":%s,"used_browse":%s,"sessions":%s,"installation_id":%s,"source":"%s","_repo_slug":"%s","_branch":"%s"}\n' \
|
|
"$TS" "$EVENT_TYPE" "$SKILL" "$SESSION_ID" "$GSTACK_VERSION" "$OS" "$ARCH" \
|
|
"$DUR_FIELD" "$OUTCOME" "$ERR_FIELD" "$ERR_MSG_FIELD" "$STEP_FIELD" \
|
|
"$BROWSE_BOOL" "${SESSIONS:-1}" \
|
|
"$INSTALL_FIELD" "$SOURCE" "$REPO_SLUG" "$BRANCH" >> "$JSONL_FILE" 2>/dev/null || true
|
|
|
|
# ─── Trigger sync if tier is not off ─────────────────────────
|
|
SYNC_CMD="$GSTACK_DIR/bin/gstack-telemetry-sync"
|
|
if [ -x "$SYNC_CMD" ]; then
|
|
"$SYNC_CMD" 2>/dev/null &
|
|
fi
|
|
|
|
exit 0
|