feat: telemetry data integrity — source tagging, UUID fingerprint, duration guards

- Add source field (live/test/dev) to telemetry pipeline: --source flag in
  gstack-telemetry-log, GSTACK_TELEMETRY_SOURCE env fallback, pass-through
  in telemetry-sync, source=eq.live filter on all dashboard queries
- Replace SHA-256 installation_id with UUID install_fingerprint for all tiers
  (not just community). Expand-contract migration: ADD new column + trigger
  to copy installation_id, preserving backward compat with old clients
- Fix duration bug: persist _TEL_START to file via $PPID (stable across bash
  blocks), cap durations at 86400s, reject negative values
- Ungate update-check pings from telemetry=off — sends only version + OS +
  random UUID. Generate .install-id in update-check for telemetry=off users
- Migration 003: source columns, install_fingerprint, duration CHECK
  constraint, indexes, recreated views with source filter, growth funnel
  (first-seen based), materialized views for daily installs + version adoption
- E2E test isolation: session-runner sets GSTACK_TELEMETRY_SOURCE=test
- 8 new telemetry tests (source field, duration caps, fingerprint persistence)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-23 15:48:25 -07:00
parent b437b531b7
commit 6bd6d5ba0f
8 changed files with 269 additions and 50 deletions
+12 -4
View File
@@ -209,8 +209,12 @@ _TEL=$(${ctx.paths.binDir}/gstack-config get telemetry 2>/dev/null || true)
_TEL_PROMPTED=$([ -f ~/.gstack/.telemetry-prompted ] && echo "yes" || echo "no")
_TEL_START=$(date +%s)
_SESSION_ID="$$-$(date +%s)"
echo $_TEL_START > ~/.gstack/analytics/.tel-start-$PPID
echo $_SESSION_ID > ~/.gstack/analytics/.tel-session-$PPID
echo "TELEMETRY: \${_TEL:-off}"
echo "TEL_PROMPTED: $_TEL_PROMPTED"
echo "TEL_START: $_TEL_START"
echo "SESSION_ID: $_SESSION_ID"
_EMAIL=$(${ctx.paths.binDir}/gstack-config get email 2>/dev/null || true)
_COMM_PROMPTED=$([ -f ~/.gstack/.community-prompted ] && echo "yes" || echo "no")
_AUTH_OK=$(${ctx.paths.binDir}/gstack-auth-refresh --check 2>/dev/null && echo "yes" || echo "no")
@@ -526,7 +530,7 @@ Hey gstack team — ran into this while using /{skill-name}:
Slug: lowercase, hyphens, max 60 chars (e.g. \`browse-js-no-await\`). Skip if file already exists. Max 3 reports per session. File inline and continue — don't stop the workflow. Tell user: "Filed gstack field report: {title}"`;
}
function generateCompletionStatus(): string {
function generateCompletionStatus(ctx: TemplateContext): string {
return `## Completion Status Protocol
When completing a skill workflow, report status using one of:
@@ -577,11 +581,15 @@ Skipping this command loses session duration and outcome data.
Run this bash:
\`\`\`bash
_TEL_START=$(cat ~/.gstack/analytics/.tel-start-$PPID 2>/dev/null || echo 0)
_SESSION_ID=$(cat ~/.gstack/analytics/.tel-session-$PPID 2>/dev/null || echo "")
_TEL_END=$(date +%s)
_TEL_DUR=$(( _TEL_END - _TEL_START ))
rm -f ~/.gstack/analytics/.tel-start-$PPID ~/.gstack/analytics/.tel-session-$PPID 2>/dev/null || true
rm -f ~/.gstack/analytics/.pending-"$_SESSION_ID" 2>/dev/null || true
~/.claude/skills/gstack/bin/gstack-telemetry-log \\
${ctx.paths.binDir}/gstack-telemetry-log \\
--skill "SKILL_NAME" --duration "$_TEL_DUR" --outcome "OUTCOME" \\
--source "\${GSTACK_TELEMETRY_SOURCE:-live}" \\
--used-browse "USED_BROWSE" --session-id "$_SESSION_ID" \\
--error-class "ERROR_CLASS" --error-message "ERROR_MESSAGE" \\
--failed-step "FAILED_STEP" 2>/dev/null &
@@ -603,7 +611,7 @@ When you are in plan mode and about to call ExitPlanMode:
3. If it does NOT — run this command:
\\\`\\\`\\\`bash
~/.claude/skills/gstack/bin/gstack-review-read
${ctx.paths.binDir}/gstack-review-read
\\\`\\\`\\\`
Then write a \`## GSTACK REVIEW REPORT\` section to the end of the plan file:
@@ -643,7 +651,7 @@ function generatePreamble(ctx: TemplateContext): string {
generateRepoModeSection(),
generateSearchBeforeBuildingSection(ctx),
generateContributorMode(),
generateCompletionStatus(),
generateCompletionStatus(ctx),
].join('\n\n');
}