fix(telemetry): report each terminal outcome once and stamp usage_context on every event - #954
Draft
santoshkumarradha wants to merge 1 commit into
Draft
Conversation
…text on every event Two defects let execution_completed count things that were not executions. Duplicate lifecycle events were forwarded verbatim. A terminal status callback re-delivered after a lost 200 used to re-run every side effect, publishing a second completed event for an execution that had already been reported (#951 closed that path in the handler; the SDKs retry a callback up to five times, so one execution could report several). The telemetry client had no defense of its own: it minted a stable telemetry_event_id and left deduplication entirely to the ingest side. It now remembers the terminal outcomes it has reported and drops a repeat, so a republished event cannot inflate a count regardless of what ingest does with the event ID. Only stable identities are eligible — the random ones belong to transitions allowed to recur, such as timeout -> running -> timeout, and collapsing those would lose real events. The set is bounded at 8192 keys with oldest-first eviction; a duplicate arrives within seconds, so eviction can only drop keys long past the window where they could suppress anything. usage_context rode only on control_plane_started. A CI job starts the control plane on a fresh volume, so it mints a new install ID and its executions look exactly like a real first-time user's — and with the context on the startup event alone, nothing downstream could separate them after ingestion. Disabling telemetry for the functional-test compose stacks was a fix for one known producer; this makes every producer distinguishable at the source. It is now stamped on every event, so execution_completed can be filtered to dev_or_local/server and CI traffic excluded. Schema version goes to 3 so the ingest side can tell a build that stamps usage_context everywhere from one that does not, and know when the filter is trustworthy. Co-authored-by: Santosh kumar <santoshkumarradha@users.noreply.github.com>
Contributor
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
Contributor
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Investigating an
execution_completedspike in product metrics (~7k/day baseline → 70k on Aug 22 → 363k on a partial Aug 23) surfaced two defects in the OSS telemetry client that let the event count things that were not executions. This fixes both: a terminal outcome is now reported once per execution, andusage_contextis stamped on every event so CI traffic can be told apart from real usage.Neither defect is the whole story of that spike — the count is aggregated by a hosted relay outside this repo — but both are real, both inflate the same metric, and both were live during the window.
Duplicate lifecycle events were forwarded verbatim. A terminal status callback re-delivered after a lost 200 used to re-run every side effect in
handleStatusUpdate, publishing a secondexecution_completedfor an execution that had already been reported. #951 closed that path in the handler on Aug 23 and measured it directly ("two duplicate succeeded callbacks and one failed callback emitted threeexecution_completedevents"); all three SDKs retry a status callback up to five times, so a single execution could report several. The telemetry client had no defense of its own — it minted a stabletelemetry_event_idand delegated deduplication entirely to the ingest side, so any republish reached the wire and counted if ingest did not honor the ID.handleExecutionEventnow remembers the terminal outcomes it has reported and drops a repeat. Only stable identities are eligible; the random ones assigned in theelsebranch belong to transitions that are allowed to recur (timeout → running → timeout) and collapsing those would lose real events. The set is bounded at 8192 keys with oldest-first eviction — a duplicate arrives within seconds of the original, so eviction can only ever drop a key long past the window in which it could still suppress anything. Keys are built from raw execution IDs and never leave the process; only the existing HMAC is sent.usage_contextrode only oncontrol_plane_started. A CI job starts the control plane on a fresh volume, so it mints a new install ID and its executions are indistinguishable from a real first-time user's. With the context carried by the startup event alone, no downstream query could separate the two after ingestion — which is why the polluted range cannot be cleaned by filtering and has to be trimmed by date instead. #942 disabled telemetry for the functional-test compose stacks, which fixes one known producer; stamping the property on every event makes every producer distinguishable at the source, including ones we have not found yet.detectUsageContextalready existed and already detected CI — it was simply never applied beyond startup.telemetry_schema_versiongoes to2→3so the ingest side can tell a build that stampsusage_contexteverywhere from one that does not, and therefore know for which date ranges ausage_contextfilter is trustworthy. This needs confirmation against the relay atagentfield.ai/api/oss/telemetrybefore merge — if that endpoint validates the version strictly rather than treating it as advisory, a bump would drop events, which is the opposite of what this PR is for.Type of change
Test plan
cd control-plane && go test ./internal/observability/ -count=1cd control-plane && go test ./internal/observability/ -count=1 -race(the suppression set is mutex-guarded)cd control-plane && go test ./internal/observability/... ./internal/handlers/... ./internal/server/... ./internal/events/... -count=1— all greencd control-plane && go build ./... && go vet ./internal/observability/New tests, all in
telemetry_test.go:TestTelemetryTerminalOutcomeReportedOnce— four identical completed/failed/cancelled events enqueue one.TestTelemetryTerminalSuppressionIsPerExecution— two executions completing both report.TestTelemetryNonTerminalEventsAreNotSuppressed— a repeatedexecution_startedis not collapsed.TestTelemetryReportedSetEvictsOldestPastCapacity— bounded growth, oldest-first eviction.TestTelemetryUsageContextStampedOnEveryEvent— execution, node, and plain enqueued events all carry it.TestTelemetryDetectsUsageContext— each CI variable, plusserveranddev_or_local. Clears the ambient CI environment first, or every case would pass on the runner's ownCI=true.TestTelemetryExecutionEventIdentityIsStableAndOpaquechanged rather than being deleted: it used to send the same event twice and read two queue entries, which now blocks because the duplicate is suppressed. It recomputes the identity a republish would carry and asserts it matches the one already sent — the property that actually mattered (ingest can recognize a re-delivery as the same outcome) — and keeps the payload-opacity assertions unchanged.Existing coverage that pins behavior this PR deliberately does not change:
TestTelemetryTimeoutEventIdentityDoesNotCollapseRepeatedTransitionsandTestTelemetryExecutionEventIdentityDoesNotCollapseMissingIDsboth still pass.Test coverage
coverage-baseline.jsonis untouched.Notes for review
usage_contextkeeps the data and makes it filterable, which seemed strictly better than dropping it; turning collection off by default in CI is a product call, and the opt-out is now documented instead.telemetry_event_idremains the cross-process idempotency mechanism, and this is a second line of defense, not a replacement.telemetryReportedSet's zero value is usable and allocates on firstobserve, so a caller constructingTelemetryServicedirectly (as every test in the package does) gets the same behavior as production rather than silently getting none.Related issues / PRs