Skip to content

Commit 809fb86

Browse files
Improve anonymous OSS telemetry (#823)
1 parent f820225 commit 809fb86

17 files changed

Lines changed: 754 additions & 42 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,8 @@ The control plane is a stateless Go service. Agents connect from anywhere - your
510510

511511
## Learn More
512512

513+
AgentField sends privacy-minimized installation and lifecycle telemetry by default; no prompts or execution payloads are sent. Set `AGENTFIELD_TELEMETRY_ENABLED=false` to disable it. [Telemetry details](docs/ENVIRONMENT_VARIABLES.md#anonymous-telemetry).
514+
513515
The thinking behind AgentField - essays on AI backends, harness orchestration, and the infrastructure production agents actually need.
514516

515517
<table>

control-plane/cmd/af/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func runServer(cmd *cobra.Command, args []string) {
5959
if err != nil {
6060
logger.Logger.Fatal().Err(err).Msg("Failed to load configuration")
6161
}
62+
cfg.Telemetry.AgentFieldVersion = version
6263

6364
// Override port from flag if provided
6465
if cmd.Flags().Lookup("port").Changed {

control-plane/cmd/agentfield-server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func runServer(cmd *cobra.Command, args []string) {
7373
if err != nil {
7474
log.Fatalf("Failed to load configuration: %v", err)
7575
}
76+
cfg.Telemetry.AgentFieldVersion = version
7677

7778
// Re-initialize logger with configured level now that config is loaded.
7879
// The CLI root command sets a default (info/debug based on --verbose),

control-plane/cmd/agentfield-server/main_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ func TestRunServer_AppliesFlagOverrides(t *testing.T) {
302302
if gotCfg.AgentField.Port != 12345 {
303303
t.Fatalf("expected env override port 12345, got %d", gotCfg.AgentField.Port)
304304
}
305+
if gotCfg.Telemetry.AgentFieldVersion != version {
306+
t.Fatalf("expected telemetry build version %q, got %q", version, gotCfg.Telemetry.AgentFieldVersion)
307+
}
305308
if gotCfg.UI.Enabled {
306309
t.Fatal("backend-only flag should disable UI")
307310
}

control-plane/config/agentfield.yaml

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

control-plane/internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ type TelemetryConfig struct {
5050
InstallIDPath string `yaml:"install_id_path" mapstructure:"install_id_path"`
5151
InstallID string `yaml:"install_id" mapstructure:"install_id"`
5252
Timeout time.Duration `yaml:"timeout" mapstructure:"timeout"`
53+
// AgentFieldVersion is runtime build metadata and is never read from config.
54+
AgentFieldVersion string `yaml:"-" mapstructure:"-"`
5355
}
5456

5557
// IsEnabled returns true unless telemetry was explicitly disabled.

control-plane/internal/config/config_additional_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ func TestTelemetryConfigDefaultsAndEnvOverrides(t *testing.T) {
188188
if cfg.Telemetry.Timeout != 800*time.Millisecond {
189189
t.Fatalf("unexpected timeout %s", cfg.Telemetry.Timeout)
190190
}
191+
if cfg.Telemetry.InstallIDPath != "" {
192+
t.Fatalf("default install ID path must resolve under AgentField home, got %q", cfg.Telemetry.InstallIDPath)
193+
}
191194

192195
t.Setenv("AGENTFIELD_TELEMETRY_ENABLED", "false")
193196
t.Setenv("AGENTFIELD_TELEMETRY_ENDPOINT", "https://example.test/telemetry")

control-plane/internal/handlers/execute.go

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ func (c *executionController) handleSync(ctx *gin.Context) {
262262
writeExecutionError(ctx, err)
263263
return
264264
}
265+
plan.executionMode = "sync"
265266

266267
if plan.replayHit != nil {
267268
if err := c.completeReplayHit(reqCtx, plan); err != nil {
@@ -692,6 +693,7 @@ func (c *executionController) handleAsync(ctx *gin.Context) {
692693
writeExecutionError(ctx, err)
693694
return
694695
}
696+
plan.executionMode = "async"
695697

696698
if plan.replayHit != nil {
697699
if err := c.completeReplayHit(reqCtx, plan); err != nil {
@@ -984,8 +986,9 @@ func (c *executionController) handleStatusUpdate(ctx *gin.Context) {
984986
}
985987

986988
eventData := map[string]interface{}{
987-
"error": req.Error,
988-
"progress": req.Progress,
989+
"error": req.Error,
990+
"progress": req.Progress,
991+
"transition_source": "status_callback",
989992
}
990993
if req.StatusReason != nil && strings.TrimSpace(*req.StatusReason) != "" {
991994
eventData["status_reason"] = strings.TrimSpace(*req.StatusReason)
@@ -1053,6 +1056,65 @@ func (c *executionController) publishExecutionEvent(exec *types.Execution, statu
10531056
c.publishExecutionEventWithReasonerInfo(exec, status, data, nil, nil)
10541057
}
10551058

1059+
// enrichExecutionLifecycleData adds low-cardinality lifecycle dimensions used by
1060+
// observability consumers. It does not mutate execution state or include payloads.
1061+
func enrichExecutionLifecycleData(data map[string]interface{}, exec *types.Execution, status string) {
1062+
if data == nil || exec == nil {
1063+
return
1064+
}
1065+
1066+
data["is_root_execution"] = exec.ParentExecutionID == nil || strings.TrimSpace(*exec.ParentExecutionID) == ""
1067+
if _, ok := data["workflow_depth"]; !ok {
1068+
if data["is_root_execution"] == true {
1069+
data["workflow_depth"] = 0
1070+
}
1071+
}
1072+
if exec.DurationMS != nil {
1073+
data["duration_ms"] = *exec.DurationMS
1074+
}
1075+
1076+
switch status {
1077+
case string(types.ExecutionStatusSucceeded):
1078+
data["outcome"] = "succeeded"
1079+
case string(types.ExecutionStatusFailed):
1080+
data["outcome"] = "failed"
1081+
data["failure_category"] = canonicalFailureCategory(exec.StatusReason, "unknown")
1082+
case string(types.ExecutionStatusCancelled):
1083+
data["outcome"] = "cancelled"
1084+
data["failure_category"] = "cancelled"
1085+
case string(types.ExecutionStatusTimeout):
1086+
data["outcome"] = "timeout"
1087+
data["failure_category"] = "timeout"
1088+
}
1089+
}
1090+
1091+
func canonicalFailureCategory(statusReason *string, fallback string) string {
1092+
if statusReason == nil {
1093+
return fallback
1094+
}
1095+
category := strings.TrimSpace(*statusReason)
1096+
if separator := strings.Index(category, ":"); separator >= 0 {
1097+
category = strings.TrimSpace(category[:separator])
1098+
}
1099+
switch category {
1100+
case string(ErrorCategoryLLMUnavailable),
1101+
string(ErrorCategoryConcurrencyLimit),
1102+
string(ErrorCategoryAgentTimeout),
1103+
string(ErrorCategoryAgentError),
1104+
string(ErrorCategoryAgentUnreachable),
1105+
string(ErrorCategoryBadResponse),
1106+
string(ErrorCategoryInternal),
1107+
"agent_restart_orphaned",
1108+
"validation",
1109+
"permission_denied",
1110+
"node_unavailable",
1111+
"target_not_found":
1112+
return category
1113+
default:
1114+
return fallback
1115+
}
1116+
}
1117+
10561118
func (c *executionController) publishExecutionEventWithReasonerInfo(exec *types.Execution, status string, data map[string]interface{}, agent *types.AgentNode, reasonerID *string) {
10571119
if exec == nil {
10581120
return
@@ -1074,6 +1136,7 @@ func (c *executionController) publishExecutionEventWithReasonerInfo(exec *types.
10741136
if data == nil {
10751137
data = make(map[string]interface{})
10761138
}
1139+
enrichExecutionLifecycleData(data, exec, status)
10771140

10781141
// Add reasoner_id to the event data
10791142
rID := exec.ReasonerID
@@ -1114,6 +1177,7 @@ func (c *executionController) publishExecutionEventWithReasonerInfo(exec *types.
11141177
}
11151178
if workflowExec, err := c.store.GetWorkflowExecution(context.Background(), exec.ExecutionID); err == nil && workflowExec != nil {
11161179
data["retry_count"] = workflowExec.RetryCount
1180+
data["workflow_depth"] = workflowExec.WorkflowDepth
11171181
}
11181182

11191183
// Add reasoner definitions if agent info is available
@@ -1200,7 +1264,9 @@ func (c *executionController) publishExecutionStartedEvent(plan *preparedExecuti
12001264
}
12011265

12021266
data := map[string]interface{}{
1203-
"target_type": plan.targetType,
1267+
"target_type": plan.targetType,
1268+
"execution_mode": plan.executionMode,
1269+
"transition_source": "execution_controller",
12041270
}
12051271

12061272
// Include input payload info (not the full payload, just metadata)
@@ -1362,6 +1428,7 @@ type preparedExecution struct {
13621428
agent *types.AgentNode
13631429
target *parsedTarget
13641430
targetType string
1431+
executionMode string
13651432
llmEndpoint string
13661433
webhookRegistered bool
13671434
webhookError *string
@@ -1723,6 +1790,9 @@ func (c *executionController) completeReplayHit(ctx context.Context, plan *prepa
17231790
}
17241791

17251792
eventData := map[string]interface{}{
1793+
"target_type": plan.targetType,
1794+
"execution_mode": plan.executionMode,
1795+
"transition_source": "replay",
17261796
"replay": map[string]interface{}{
17271797
"source_execution_id": plan.replayHit.SourceExecutionID,
17281798
"source_run_id": plan.replayHit.SourceRunID,
@@ -1905,7 +1975,11 @@ func (c *executionController) completeExecution(ctx context.Context, plan *prepa
19051975
if plan.webhookRegistered || (updated != nil && updated.WebhookRegistered) {
19061976
c.triggerWebhook(plan.exec.ExecutionID)
19071977
}
1908-
eventData := map[string]interface{}{}
1978+
eventData := map[string]interface{}{
1979+
"target_type": plan.targetType,
1980+
"execution_mode": plan.executionMode,
1981+
"transition_source": "execution_controller",
1982+
}
19091983
if !c.redactPayloads {
19101984
if payload := decodeJSON(result); payload != nil {
19111985
eventData["result"] = payload
@@ -1990,7 +2064,11 @@ func (c *executionController) failExecution(ctx context.Context, plan *preparedE
19902064
c.triggerWebhook(plan.exec.ExecutionID)
19912065
}
19922066
eventData := map[string]interface{}{
1993-
"error": errMsg,
2067+
"error": errMsg,
2068+
"target_type": plan.targetType,
2069+
"execution_mode": plan.executionMode,
2070+
"failure_category": string(category),
2071+
"transition_source": "execution_controller",
19942072
}
19952073
if !c.redactPayloads {
19962074
if payload := decodeJSON(result); payload != nil {

control-plane/internal/handlers/execute_cancel.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,15 @@ func CancelExecutionHandler(store ExecutionStore) gin.HandlerFunc {
115115
}
116116
}
117117

118-
events.PublishExecutionCancelled(executionID, exec.RunID, exec.AgentNodeID, map[string]interface{}{"reason": reason})
118+
eventData := map[string]interface{}{
119+
"reason": reason,
120+
"transition_source": "cancel_api",
121+
}
122+
enrichExecutionLifecycleData(eventData, exec, string(types.ExecutionStatusCancelled))
123+
if wfExec != nil {
124+
eventData["workflow_depth"] = wfExec.WorkflowDepth
125+
}
126+
events.PublishExecutionCancelled(executionID, exec.RunID, exec.AgentNodeID, eventData)
119127

120128
payload, marshalErr := json.Marshal(map[string]interface{}{
121129
"reason": reason,

control-plane/internal/handlers/execute_cancel_tree.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,16 @@ func cancelOneExecution(
212212
}
213213
}
214214

215-
events.PublishExecutionCancelled(updated.ExecutionID, updated.RunID, updated.AgentNodeID, map[string]interface{}{
216-
"reason": reasonRaw,
217-
"source": "cancel_tree",
218-
})
215+
eventData := map[string]interface{}{
216+
"reason": reasonRaw,
217+
"source": "cancel_tree",
218+
"transition_source": "cancel_tree",
219+
}
220+
enrichExecutionLifecycleData(eventData, updated, string(types.ExecutionStatusCancelled))
221+
if wfExec != nil {
222+
eventData["workflow_depth"] = wfExec.WorkflowDepth
223+
}
224+
events.PublishExecutionCancelled(updated.ExecutionID, updated.RunID, updated.AgentNodeID, eventData)
219225

220226
payload, marshalErr := json.Marshal(map[string]interface{}{
221227
"reason": reasonRaw,

0 commit comments

Comments
 (0)