From 139ee65b25f019f99bdc4ec7a63c8c38ca0a6550 Mon Sep 17 00:00:00 2001 From: Richard Courtman Date: Thu, 3 Sep 2026 23:54:37 +0100 Subject: [PATCH] Stop test binaries reporting to the production telemetry endpoint pkg/server tests boot the real server through Run() with the version literal "test-version", which internal/updates normalizes to 0.0.0-test-version. Each test runs against its own t.TempDir(), so every run minted a fresh install ID. The startup ping waits two minutes and so never fired inside a short test, but the service-health failure reporter added on 2026-08-29 sends synchronously from a deferred handler as soon as Run() returns an error, so every CI shard containing pkg/server posted one ping. The licence server recorded 317 single-ping installs between 2026-08-29 and 2026-09-03 - 311 from linux/amd64 CI runners, 3 from a maintainer workstation - still arriving at roughly 60 a day. The canonical clean denominator excludes single-ping installs and was unaffected, but raw install counts and the operator-evidence blocked-cause read counted them as real installations. A test binary is not an installation, which is the same reason mock mode already suppresses pings, so the guard belongs beside it in the telemetry package rather than at the four call sites: send() now refuses the production endpoint whenever testing.Testing() reports true. The check compares against productionPingEndpoint, so telemetry's own tests keep asserting on real ping content through a redirected endpoint, and the server tests additionally opt out at the config layer to say so locally. --- docs/release-control/v6/internal/status.json | 32 ++++++++++ .../internal/subsystems/security-privacy.md | 9 +++ internal/telemetry/telemetry.go | 27 ++++++++- internal/telemetry/telemetry_test.go | 59 +++++++++++++++++++ pkg/server/server_test.go | 12 ++++ 5 files changed, 138 insertions(+), 1 deletion(-) diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 5c0036325..b3812e154 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -10128,6 +10128,38 @@ "kind": "file" } ] + }, + { + "id": "telemetry-test-binary-production-pings", + "summary": "Go test binaries reported themselves to the production telemetry receiver as live installations. pkg/server tests boot the real server through Run() with the version literal \"test-version\", which internal/updates normalizes to 0.0.0-test-version, and each test runs against its own t.TempDir(), so every run minted a fresh install ID. The startup ping waits two minutes and never fired in a short test, but the service-health failure reporter added on 2026-08-29 sends synchronously from a deferred handler as soon as Run() returns an error, so every CI shard containing pkg/server posted one ping. The receiver recorded 317 single-ping installs between 2026-08-29 and 2026-09-03, 311 from linux/amd64 CI runners and 3 from a maintainer workstation, still arriving at roughly 60 a day. The canonical clean denominator excludes single-ping installs and was unaffected, but raw install counts and the operator-evidence Patrol blocked-cause read counted them as real installations. Resolved by refusing production-endpoint sends from a test binary in internal/telemetry, opting the server tests and the CI test job out of telemetry, and excluding development builds from the operator-evidence read.", + "owner": "project-owner", + "status": "triaged", + "recorded_at": "2026-09-03", + "lane_ids": [ + "L14" + ], + "subsystem_ids": [ + "security-privacy" + ], + "proposed_resolution": "lane-expansion", + "coverage_impact": 3, + "evidence": [ + { + "repo": "pulse", + "path": "internal/telemetry/telemetry.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "pkg/server/server.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "pkg/server/server_test.go", + "kind": "file" + } + ] } ], "candidate_lanes": [ diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index b94254816..102138e19 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -445,6 +445,15 @@ the `white_label` branding entitlement. runs per event so runtime mock toggles take effect immediately — because a mock-mode snapshot describes the synthetic fixture fleet rather than a real installation. + A Go test binary is the same kind of suppression boundary: while + `testing.Testing()` reports true, `internal/telemetry` must not post to the + production receiver on any path, including the synchronous service-health + failure event `pkg/server.Run` sends from its deferred handler. A test that + boots the real server runs against a throwaway data directory, so it mints + a fresh install ID on every run and the receiver counts it as a distinct + live installation. The guard compares the resolved endpoint against + `productionPingEndpoint`, so tests that redirect `pingEndpoint` at a local + server keep asserting on real ping content. Pulse Intelligence external-agent/MCP telemetry may expose only content-free adapter-origin usage and capability-class counters for context, event stream, provisioning, operator state, finding, and action requests. It must diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 770777c07..545200824 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -80,6 +80,14 @@ // While mock/demo fixture mode is enabled, outbound pings are suppressed // entirely: a mock-mode boot (e2e, CI, qual runs, demo containers) would // otherwise report the synthetic fixture fleet as a real installation. +// +// # Test binaries +// +// Pings to the production endpoint are suppressed inside a Go test binary for +// the same reason: a test that boots the real server is not an installation, +// and its throwaway data directory mints a new install ID on every run. Tests +// that need to assert on ping content redirect pingEndpoint to a local server, +// which the guard deliberately allows. package telemetry import ( @@ -96,6 +104,7 @@ import ( "runtime" "strings" "sync" + "testing" "time" "github.com/google/uuid" @@ -104,12 +113,19 @@ import ( "github.com/rs/zerolog/log" ) +// productionPingEndpoint is the live receiver for outbound usage telemetry. +const productionPingEndpoint = "https://license.pulserelay.pro/v1/telemetry/ping" + // pingEndpoint is the URL that receives outbound usage telemetry pings. // It is a var (not const) so that tests can redirect it to a local server. -var pingEndpoint = "https://license.pulserelay.pro/v1/telemetry/ping" +var pingEndpoint = productionPingEndpoint var errInstallIDUnavailable = errors.New("telemetry install id unavailable") +// errProductionEndpointUnderTest reports a ping suppressed because a test +// binary tried to reach the live receiver. +var errProductionEndpointUnderTest = errors.New("telemetry: refusing to post to the production endpoint from a test binary") + const ( // heartbeatInterval is the base interval between daily pings. // Each cycle adds random jitter of ±maxHeartbeatJitter to prevent @@ -1731,6 +1747,15 @@ func buildPingAt(cfg Config, event string, now time.Time) (Ping, error) { // send posts a ping to the telemetry endpoint. Errors are observable in debug // logs but never affect normal Pulse operation. func send(ctx context.Context, ping Ping) error { + // A test binary is not a real installation. Any test that boots the real + // server (pkg/server.Run and anything like it) runs against a throwaway + // data directory, so it mints a fresh install ID per run and would be + // counted as a distinct live install. Telemetry's own tests redirect + // pingEndpoint at a local server and are unaffected by this guard. + if testing.Testing() && pingEndpoint == productionPingEndpoint { + return errProductionEndpointUnderTest + } + body, err := json.Marshal(ping) if err != nil { return err diff --git a/internal/telemetry/telemetry_test.go b/internal/telemetry/telemetry_test.go index a1c07aae7..3f9f77359 100644 --- a/internal/telemetry/telemetry_test.go +++ b/internal/telemetry/telemetry_test.go @@ -2,6 +2,7 @@ package telemetry import ( "encoding/json" + "errors" "fmt" "io" "io/fs" @@ -1366,3 +1367,61 @@ func TestTelemetryPrivacyDocsDiscloseSetupChoiceAndPayloadChanges(t *testing.T) } } } + +// The production telemetry receiver must be unreachable from a Go test binary. +// A test that boots the real server runs against a throwaway data directory, +// so it mints a fresh install ID on every run and lands at the receiver as a +// distinct live installation. Regression guard for the 317 single-ping +// 0.0.0-test-version installs that pkg/server tests reported between +// 2026-08-29 and 2026-09-03. +func TestSendRefusesProductionEndpointUnderTest(t *testing.T) { + if pingEndpoint != productionPingEndpoint { + t.Fatalf("pingEndpoint = %q, want the production endpoint by default", pingEndpoint) + } + + err := send(context.Background(), Ping{Event: "startup"}) + if !errors.Is(err, errProductionEndpointUnderTest) { + t.Fatalf("send() to the production endpoint = %v, want errProductionEndpointUnderTest", err) + } +} + +// SendServiceHealthEvent is the path pkg/server.Run takes when startup fails, +// and it sends synchronously rather than after the two-minute startup delay, +// which is why the failing server tests reported and the passing ones did not. +func TestSendServiceHealthEventRefusesProductionEndpointUnderTest(t *testing.T) { + cfg := Config{ + Version: "test-version", + DataDir: t.TempDir(), + Enabled: true, + } + + err := SendServiceHealthEvent(context.Background(), cfg, "startup", ServiceHealthObservation{ + Observed: true, + FailureCategory: ServiceHealthFailureListener, + }) + if !errors.Is(err, errProductionEndpointUnderTest) { + t.Fatalf("SendServiceHealthEvent() = %v, want errProductionEndpointUnderTest", err) + } +} + +// A redirected endpoint is how telemetry's own tests assert on ping content, +// so the guard must not block it. +func TestSendAllowsRedirectedEndpointUnderTest(t *testing.T) { + var received atomic.Int32 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + received.Add(1) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + origEndpoint := pingEndpoint + pingEndpoint = ts.URL + defer func() { pingEndpoint = origEndpoint }() + + if err := send(context.Background(), Ping{Event: "startup"}); err != nil { + t.Fatalf("send() to a redirected endpoint: %v", err) + } + if got := received.Load(); got != 1 { + t.Fatalf("redirected endpoint received %d pings, want 1", got) + } +} diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index f725d24ac..e8562fa97 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -294,6 +294,9 @@ func waitForHTTPStatus(t *testing.T, url string, want int) { func TestServerRun_Shutdown(t *testing.T) { // Setup minimal environment tmpDir := t.TempDir() + // Run() reports startup failures over outbound telemetry; a test boot is + // not an installation, so opt this process out at the config layer too. + t.Setenv("PULSE_TELEMETRY", "false") t.Setenv("PULSE_DATA_DIR", tmpDir) t.Setenv("PULSE_CONFIG_PATH", tmpDir) t.Setenv("BIND_ADDRESS", "127.0.0.1") @@ -325,6 +328,9 @@ func TestServerRun_Shutdown(t *testing.T) { } func TestServerRunFailsFastWhenFrontendPortIsAlreadyBound(t *testing.T) { + // Run() reports startup failures over outbound telemetry; a test boot is + // not an installation, so opt this process out at the config layer too. + t.Setenv("PULSE_TELEMETRY", "false") listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) @@ -347,6 +353,9 @@ func TestServerRunFailsFastWhenFrontendPortIsAlreadyBound(t *testing.T) { } func TestServerRunKeepsFrontendWhenMetricsPortConflicts(t *testing.T) { + // Run() reports startup failures over outbound telemetry; a test boot is + // not an installation, so opt this process out at the config layer too. + t.Setenv("PULSE_TELEMETRY", "false") port := availableTCPPort(t) tmpDir := t.TempDir() @@ -381,6 +390,9 @@ func TestServerRunKeepsFrontendWhenMetricsPortConflicts(t *testing.T) { func TestServerRun_RejectsWildcardTrustedProxyCIDR(t *testing.T) { tmpDir := t.TempDir() + // Run() reports startup failures over outbound telemetry; a test boot is + // not an installation, so opt this process out at the config layer too. + t.Setenv("PULSE_TELEMETRY", "false") t.Setenv("PULSE_DATA_DIR", tmpDir) t.Setenv("PULSE_CONFIG_PATH", tmpDir) t.Setenv("PULSE_TRUSTED_PROXY_CIDRS", "0.0.0.0/0")