159 lines
6.7 KiB
Plaintext
159 lines
6.7 KiB
Plaintext
|
|
#!/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=""
|
||
|
|
EVENT_TYPE="skill_run"
|
||
|
|
|
||
|
|
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 ;;
|
||
|
|
--event-type) EVENT_TYPE="$2"; shift 2 ;;
|
||
|
|
*) shift ;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
# ─── 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
|
||
|
|
INSTALL_ID=""
|
||
|
|
if [ "$TIER" = "community" ]; then
|
||
|
|
HOST="$(hostname 2>/dev/null || echo "unknown")"
|
||
|
|
USER="$(whoami 2>/dev/null || echo "unknown")"
|
||
|
|
if command -v shasum >/dev/null 2>&1; then
|
||
|
|
INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | shasum -a 256 | awk '{print $1}')"
|
||
|
|
elif command -v sha256sum >/dev/null 2>&1; then
|
||
|
|
INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | sha256sum | awk '{print $1}')"
|
||
|
|
elif command -v openssl >/dev/null 2>&1; then
|
||
|
|
INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | openssl dgst -sha256 | awk '{print $NF}')"
|
||
|
|
fi
|
||
|
|
# If no SHA-256 command available, install_id stays empty
|
||
|
|
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\""
|
||
|
|
|
||
|
|
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,"used_browse":%s,"sessions":%s,"installation_id":%s,"_repo_slug":"%s","_branch":"%s"}\n' \
|
||
|
|
"$TS" "$EVENT_TYPE" "$SKILL" "$SESSION_ID" "$GSTACK_VERSION" "$OS" "$ARCH" \
|
||
|
|
"$DUR_FIELD" "$OUTCOME" "$ERR_FIELD" "$BROWSE_BOOL" "${SESSIONS:-1}" \
|
||
|
|
"$INSTALL_FIELD" "$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
|