fix(telemetry): suppress reporting from automated environments (#129155)

CI jobs are not installs; unchecked they outnumber operators by orders of
magnitude and make version and platform counts meaningless. A configured
telemetry endpoint still reports, so update-path E2E lanes keep working.
This commit is contained in:
Peter Steinberger
2026-08-25 01:37:48 -07:00
committed by GitHub
parent bb0dc4629a
commit 6885eb69b0
4 changed files with 67 additions and 2 deletions
+13
View File
@@ -165,6 +165,19 @@ even when `telemetry.enabled` is `true`. `DO_NOT_TRACK` does not disable the
daily update check: OpenClaw sends the update-only `GET` request without a
feature-statistics body.
## Automated environments
OpenClaw sends nothing when it detects an automated environment, meaning the
`CI` environment variable is set to a truthy value. Continuous integration jobs
are not installations: they would outnumber real operators by orders of
magnitude and make version and platform counts meaningless, and your pipeline
should not report to us on every job.
This applies to both tiers, so a CI job sends no update check and no feature
statistics. Setting `OPENCLAW_TELEMETRY_ENDPOINT` overrides the suppression,
because a configured endpoint means the run is deliberately exercising this
path.
## Disable every automatic update request
To go fully dark, disable the existing startup update check:
+1
View File
@@ -10,6 +10,7 @@ import { runCommandWithRuntime } from "./cli-utils.js";
const TELEMETRY_REASON_LABELS = {
enabled: "enabled in configuration",
"automated-environment": "disabled in an automated environment (CI is set)",
"do-not-track": "disabled by DO_NOT_TRACK",
"config-disabled": "disabled in configuration",
"never-asked": "consent has not been requested",
+35 -1
View File
@@ -12,7 +12,11 @@ import {
type OpenClawTestState,
} from "../test-utils/openclaw-test-state.js";
import { VERSION } from "../version.js";
import { buildTelemetryPayload, checkTelemetryUpdate } from "./telemetry.js";
import {
buildTelemetryPayload,
checkTelemetryUpdate,
resolveTelemetryStatus,
} from "./telemetry.js";
const NOW = Date.parse("2026-08-23T12:00:00.000Z");
const DAY_MS = 24 * 60 * 60 * 1000;
@@ -86,6 +90,7 @@ describe("anonymous telemetry", () => {
layout: "state-only",
prefix: "openclaw-telemetry-",
env: {
CI: undefined,
DO_NOT_TRACK: undefined,
OPENCLAW_NIX_MODE: undefined,
OPENCLAW_NO_AUTO_UPDATE: undefined,
@@ -360,6 +365,35 @@ describe("anonymous telemetry", () => {
expect(readConfigMachineState(TELEMETRY_STATE_KEY)).toBeUndefined();
});
it("never sends a request from an automated environment", async () => {
setTestEnvValue("CI", "true");
await expect(
checkTelemetryUpdate(createFeatureConfig(), {
surface: "gateway",
fetchImpl: globalThis.fetch,
nowMs: NOW,
}),
).resolves.toBeNull();
expect(mockHttp.requests()).toHaveLength(0);
expect(readConfigMachineState(TELEMETRY_STATE_KEY)).toBeUndefined();
expect(resolveTelemetryStatus(createFeatureConfig()).reason).toBe("automated-environment");
});
it("still reports from an automated environment when an endpoint is configured for it", async () => {
const customEndpoint = "https://telemetry.example.invalid/api/latest-version";
setTestEnvValue("CI", "true");
setTestEnvValue("OPENCLAW_TELEMETRY_ENDPOINT", customEndpoint);
mockHttp.intercept({ url: customEndpoint, reply: { json: { version: "2026.8.24" } } });
await expect(
checkTelemetryUpdate({}, { surface: "gateway", fetchImpl: globalThis.fetch, nowMs: NOW }),
).resolves.toEqual({ version: "2026.8.24" });
expect(mockHttp.requests()).toHaveLength(1);
});
it("never sends a request for Nix-managed installations", async () => {
setTestEnvValue("OPENCLAW_NIX_MODE", "1");
+18 -1
View File
@@ -54,6 +54,7 @@ type TelemetryPayload = {
type TelemetryStatusReason =
| "enabled"
| "automated-environment"
| "do-not-track"
| "config-disabled"
| "never-asked"
@@ -73,10 +74,24 @@ const TelemetryResponseSchema = z.object({
let lastFailedAttempt: { at: number; endpoint: string; stateDirectory?: string } | undefined;
let inFlightUpdate: Promise<TelemetryUpdate | null> | undefined;
/**
* CI jobs are not installs. Left unchecked they outnumber operators by orders of
* magnitude and make version and platform counts meaningless, and someone else's
* pipeline should not report to us on every job either. A configured endpoint
* means the caller is deliberately exercising this path, so it still reports.
*/
function isAutomatedEnvironment(): boolean {
if (process.env.OPENCLAW_TELEMETRY_ENDPOINT?.trim()) {
return false;
}
return isTruthyEnvValue(process.env.CI);
}
function isUpdateCheckDisabled(config: OpenClawConfig): boolean {
return (
config.update?.checkOnStart === false ||
isTruthyEnvValue(process.env.OPENCLAW_NO_AUTO_UPDATE) ||
isAutomatedEnvironment() ||
resolveIsNixMode()
);
}
@@ -132,7 +147,9 @@ export function resolveTelemetryStatus(config: OpenClawConfig): {
lastPingAt?: number;
} {
let reason: TelemetryStatusReason;
if (isUpdateCheckDisabled(config)) {
if (isAutomatedEnvironment()) {
reason = "automated-environment";
} else if (isUpdateCheckDisabled(config)) {
reason = "update-disabled";
} else if (isDoNotTrackEnabled()) {
reason = "do-not-track";