From 4c25177ddd9a15b40c7187c46339ca0a1ee04f1b Mon Sep 17 00:00:00 2001 From: Onyeka Nwamba Date: Wed, 27 May 2026 01:07:37 +0100 Subject: [PATCH] fix: suppress telemetry when running in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createTelemetry() now returns the noop client whenever CI=true or GITHUB_ACTIONS=true is set in the environment. Every major CI provider sets CI=true; the explicit GITHUB_ACTIONS check is belt-and-braces in case a future provider drops the convention. PostHog data shows that all 2,784 events to date have come from CI runs (100% linux, 100% US, all from Azure datacenter cities). This guard prevents the same leak even if AGENTRAIL_TELEMETRY_DISABLED is not set at the workflow level — for example when contributors run \`npm test\` locally with a real install ID in their config. If a downstream user genuinely wants to run AgentRail in their own CI with telemetry enabled, they can unset CI before invoking the CLI. The conventional behaviour matches what Honeycomb, Sentry, and most analytics SDKs do by default. --- src/telemetry.ts | 12 ++++++++ test/telemetry.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/telemetry.ts b/src/telemetry.ts index dd4993c..bdf99cf 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -213,6 +213,7 @@ export function createTelemetry({ if ( config.enabled === false || isTelemetryDisabledByEnv(env) + || isRunningInCI(env) || !INSTALL_ID_PATTERN.test(config.installId) || !resolvedSink ) { @@ -332,6 +333,17 @@ function isTelemetryDisabledByEnv(env: Record): bool return value !== undefined && DISABLED_ENV_VALUES.has(value); } +function isRunningInCI(env: Record): boolean { + // Most CI providers set `CI=true` (GitHub Actions, CircleCI, GitLab, + // Travis, Buildkite, Jenkins via plugins, etc.). We also check the + // GitHub Actions-specific marker as belt-and-braces, in case some + // future provider drops the conventional `CI` variable. + return ( + env.CI?.trim().toLowerCase() === "true" + || env.GITHUB_ACTIONS?.trim().toLowerCase() === "true" + ); +} + function resolveConfiguredHost( env: Record, config: TelemetryConfig, diff --git a/test/telemetry.test.ts b/test/telemetry.test.ts index 2f4ce07..4977981 100644 --- a/test/telemetry.test.ts +++ b/test/telemetry.test.ts @@ -196,6 +196,73 @@ test("AGENTRAIL_TELEMETRY_DISABLED=1 wins over enabled config", async () => { assert.deepEqual(delivered, []); }); +test("CI=true suppresses telemetry even with enabled config", async () => { + const delivered: TelemetrySinkEvent[] = []; + const telemetry = createTelemetry({ + source: "cli", + version: "0.1.6", + config: { + enabled: true, + installId, + provider: "posthog", + host: DEFAULT_POSTHOG_HOST, + }, + env: { CI: "true" }, + sink: async (event) => { + delivered.push(event); + }, + }); + + await telemetry.capture("init_started", { provider: "github" }); + + assert.deepEqual(delivered, []); +}); + +test("GITHUB_ACTIONS=true suppresses telemetry even without CI variable", async () => { + const delivered: TelemetrySinkEvent[] = []; + const telemetry = createTelemetry({ + source: "cli", + version: "0.1.6", + config: { + enabled: true, + installId, + provider: "posthog", + host: DEFAULT_POSTHOG_HOST, + }, + env: { GITHUB_ACTIONS: "true" }, + sink: async (event) => { + delivered.push(event); + }, + }); + + await telemetry.capture("init_started", { provider: "github" }); + + assert.deepEqual(delivered, []); +}); + +test("CI=false does not suppress telemetry", async () => { + const delivered: TelemetrySinkEvent[] = []; + const telemetry = createTelemetry({ + source: "cli", + version: "0.1.6", + config: { + enabled: true, + installId, + provider: "posthog", + host: DEFAULT_POSTHOG_HOST, + }, + env: { CI: "false" }, + sink: async (event) => { + delivered.push(event); + }, + }); + + await telemetry.capture("init_started", { provider: "github" }); + + assert.equal(delivered.length, 1); + assert.equal(delivered[0].event, "init_started"); +}); + test("invalid install id telemetry sends nothing", async () => { const delivered: TelemetrySinkEvent[] = []; const telemetry = createTelemetry({