Files
heicode/heicode/controller/telemetry_retention.go
T
chenchenandClaude Opus 4.8 2e37495133 feat(telemetry): retention purge + context field whitelist + size caps (#32)
Telemetry up-gating hardening (code portion of #32):

- Context field whitelist: telemetry `context` is filtered to a small set of
  non-content diagnostic keys (route/retryable/phase/exit_code/duration_ms/
  attempt) before persistence. Unknown keys — including potentially identifying
  ones (email, full file path, prompt, raw IP) — are dropped, so a client
  regression cannot land arbitrary JSON in the store. Empty/unparseable/no-allowed-key
  context is dropped to "".
- Per-field size cap: stack_top and context are truncated to 8KiB after
  redaction (backstop against unbounded blobs within batch limits).
- Retention: daily master-only task deletes telemetry rows older than
  HEICODE_TELEMETRY_RETENTION_DAYS (default 30; <=0 disables).
  HEICODE_TELEMETRY_RETENTION_INTERVAL_HOURS (default 24) sets cadence.
  model.DeleteTelemetryEventsBefore(cutoff) + controller.StartTelemetryRetentionTask()
  wired into main.go under IsMasterNode.
- GET /api/heicode/config telemetry block now surfaces retention_days for
  client/admin transparency.

Tests: whitelist drop/keep, size cap, redaction-within-allowed-key. go build/vet
clean; controller telemetry tests pass.

Affects: Manager only (telemetry ingest + retention). No billing/consume-log
change (telemetry still never bills). Privacy-doc disclosure + production
enable-checklist portions of #32 tracked in heicodeDocs sync (#34) / desktop
client API docs (#35).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:40:23 +08:00

78 lines
2.5 KiB
Go

package controller
import (
"fmt"
"sync"
"time"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// Telemetry retention (#32). Account-linkable client error-telemetry must not be
// kept indefinitely: a daily task deletes rows older than the retention window.
// Disabled when telemetry ingest is off (default) or retention <= 0.
//
// - HEICODE_TELEMETRY_RETENTION_DAYS (default 30): rows received earlier than
// now-RETENTION are deleted. <=0 disables the purge (keep-forever — only for
// explicit operator opt-out; not recommended for production).
// - HEICODE_TELEMETRY_RETENTION_INTERVAL_HOURS (default 24): sweep cadence.
//
// Master-only (wired from main.go under IsMasterNode) so multiple nodes don't
// all sweep the shared LOG_DB.
var telemetryRetentionTaskOnce sync.Once
func telemetryRetentionDays() int {
return common.GetEnvOrDefault("HEICODE_TELEMETRY_RETENTION_DAYS", 30)
}
// StartTelemetryRetentionTask launches the daily telemetry retention sweep.
func StartTelemetryRetentionTask() {
telemetryRetentionTaskOnce.Do(func() {
// Only meaningful once ingest is enabled; if the endpoint is off there is
// nothing being written, but we still allow the sweep to drain any rows
// captured during a prior enabled window. Gate on retention days instead.
days := telemetryRetentionDays()
if days <= 0 {
common.SysLog("telemetry retention task disabled (HEICODE_TELEMETRY_RETENTION_DAYS<=0; rows kept indefinitely)")
return
}
intervalHours := common.GetEnvOrDefault("HEICODE_TELEMETRY_RETENTION_INTERVAL_HOURS", 24)
if intervalHours < 1 {
intervalHours = 24
}
go func() {
time.Sleep(5 * time.Minute) // avoid startup churn
runTelemetryRetentionOnce()
ticker := time.NewTicker(time.Duration(intervalHours) * time.Hour)
defer ticker.Stop()
for range ticker.C {
runTelemetryRetentionOnce()
}
}()
common.SysLog(fmt.Sprintf("telemetry retention task started: retention=%dd interval=%dh", days, intervalHours))
})
}
func runTelemetryRetentionOnce() {
defer func() {
if r := recover(); r != nil {
common.SysLog(fmt.Sprintf("telemetry retention task panic recovered: %v", r))
}
}()
days := telemetryRetentionDays()
if days <= 0 {
return
}
cutoff := time.Now().Unix() - int64(days)*86400
deleted, err := model.DeleteTelemetryEventsBefore(cutoff)
if err != nil {
common.SysLog("telemetry retention task: " + err.Error())
return
}
if deleted > 0 {
common.SysLog(fmt.Sprintf("telemetry retention task: deleted %d events older than %d days", deleted, days))
}
}