fix(desktop): inline telemetry config into the packaged sidecar binary (#12925)

* fix(desktop): inline telemetry config into the packaged sidecar binary

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(desktop): reject non-http OTLP endpoints in the telemetry selfcheck

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
This commit is contained in:
Saoud Rizwan
2026-08-04 20:43:01 -07:00
committed by GitHub
parent 472f9c88c5
commit ddfb67515b
7 changed files with 334 additions and 3 deletions
+42
View File
@@ -225,6 +225,20 @@ jobs:
working-directory: apps/examples/desktop-app
run: bunx tauri build --target universal-apple-darwin --config src-tauri/tauri.release.conf.json
env:
# Telemetry config for the sidecar binary. Tauri's beforeBuildCommand
# (`bun run build` -> build:sidecar:bin) compiles the sidecar during
# this step and inlines these values into the binary via `--define`
# (scripts/telemetry-define-args.ts); a packaged app launched from
# Finder/the Dock has no runtime env, so build-time inlining is the
# only way the shipped sidecar can ever report telemetry.
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
OTEL_LOGS_EXPORTER: otlp
OTEL_METRICS_EXPORTER: otlp
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
# Developer ID signing (Tauri imports the cert into a temp keychain)
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@@ -262,6 +276,34 @@ jobs:
esac
done
# Guardrail: assert the telemetry config actually made it into the
# compiled sidecar. Missing env on the build step (or a regression in
# the --define inlining) would otherwise ship a release with telemetry
# silently disabled — exactly what happened for every release before
# this check existed. Being enabled is not enough on its own: an empty,
# malformed, or non-http(s) OTLP endpoint would still drop every event
# at runtime (the SDK exporters speak OTLP http/json only), so the
# selfcheck must also report a usable endpoint host.
- name: Verify sidecar telemetry config was inlined
working-directory: apps/examples/desktop-app
run: |
SELFCHECK=$(./src-tauri/bin/code-sidecar-universal-apple-darwin --telemetry-selfcheck)
echo "$SELFCHECK"
if ! printf '%s' "$SELFCHECK" | grep -q '"enabled":true'; then
echo "Packaged sidecar reports telemetry disabled."
echo "Check the OTEL_* / TELEMETRY_SERVICE_API_KEY env on the"
echo "'Build, sign, and notarize desktop bundle' step and the"
echo "--define inlining in scripts/build-sidecar-bin.ts."
exit 1
fi
if printf '%s' "$SELFCHECK" | grep -Eq '"otlp_endpoint_host":"(invalid-endpoint-url)?"'; then
echo "Packaged sidecar reports telemetry enabled but its OTLP"
echo "endpoint is missing, unparseable, or not an http(s) URL, so"
echo "every event would be dropped at runtime. Check the"
echo "OTEL_EXPORTER_OTLP_ENDPOINT secret."
exit 1
fi
- name: Collect artifacts
working-directory: apps/examples/desktop-app
env:
@@ -1,4 +1,5 @@
import { $ } from "bun";
import { telemetryDefineArgs } from "./telemetry-define-args";
const resolveTargetTriple = async (): Promise<string> => {
const fromEnv = process.env.TAURI_ENV_TARGET_TRIPLE ?? process.env.TARGET;
@@ -40,10 +41,15 @@ const sidecarOutfile = (targetTriple: string): string => {
const buildSidecar = async (targetTriple: string): Promise<string> => {
const outfile = sidecarOutfile(targetTriple);
const bunTarget = resolveBunCompileTarget(targetTriple);
// Telemetry config must be inlined into the compiled binary: a packaged
// app launched from Finder/the Dock has no OTEL_* env at runtime, so
// without this the sidecar silently ships with telemetry disabled.
// Verify with `<binary> --telemetry-selfcheck` after building.
const defines = telemetryDefineArgs();
if (bunTarget) {
await $`bun build ./sidecar/index.ts --compile --target=${bunTarget} --outfile ${outfile}`;
await $`bun build ./sidecar/index.ts --compile --target=${bunTarget} ${defines} --outfile ${outfile}`;
} else {
await $`bun build ./sidecar/index.ts --compile --outfile ${outfile}`;
await $`bun build ./sidecar/index.ts --compile ${defines} --outfile ${outfile}`;
}
return outfile;
};
@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import { telemetryDefineArgs } from "./telemetry-define-args";
function defineMap(args: string[]): Record<string, string> {
const map: Record<string, string> = {};
for (let index = 0; index < args.length; index += 2) {
expect(args[index]).toBe("--define");
const pair = args[index + 1];
const separator = pair.indexOf("=");
map[pair.slice(0, separator)] = pair.slice(separator + 1);
}
return map;
}
describe("telemetryDefineArgs", () => {
it("inlines every OTEL var getTelemetryBuildTimeConfig reads", () => {
const defines = defineMap(
telemetryDefineArgs({
OTEL_TELEMETRY_ENABLED: "1",
OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.example.com:4318",
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
OTEL_EXPORTER_OTLP_HEADERS: "x-api-key=secret",
OTEL_LOGS_EXPORTER: "otlp",
OTEL_METRICS_EXPORTER: "otlp",
OTEL_TRACES_EXPORTER: "otlp",
OTEL_METRIC_EXPORT_INTERVAL: "60000",
}),
);
expect(defines["process.env.OTEL_TELEMETRY_ENABLED"]).toBe('"1"');
expect(defines["process.env.OTEL_EXPORTER_OTLP_ENDPOINT"]).toBe(
'"https://otel.example.com:4318"',
);
expect(defines["process.env.OTEL_EXPORTER_OTLP_PROTOCOL"]).toBe(
'"http/json"',
);
expect(defines["process.env.OTEL_EXPORTER_OTLP_HEADERS"]).toBe(
'"x-api-key=secret"',
);
expect(defines["process.env.OTEL_LOGS_EXPORTER"]).toBe('"otlp"');
expect(defines["process.env.OTEL_METRICS_EXPORTER"]).toBe('"otlp"');
expect(defines["process.env.OTEL_TRACES_EXPORTER"]).toBe('"otlp"');
expect(defines["process.env.OTEL_METRIC_EXPORT_INTERVAL"]).toBe('"60000"');
});
it("inlines unset OTEL vars as empty strings so binaries never fall back to runtime env", () => {
const defines = defineMap(telemetryDefineArgs({}));
expect(defines["process.env.OTEL_TELEMETRY_ENABLED"]).toBe('""');
expect(defines["process.env.OTEL_EXPORTER_OTLP_ENDPOINT"]).toBe('""');
});
it("includes API key defines only when the vars are set", () => {
const withoutKeys = defineMap(telemetryDefineArgs({}));
expect(withoutKeys).not.toHaveProperty(
"process.env.TELEMETRY_SERVICE_API_KEY",
);
expect(withoutKeys).not.toHaveProperty("process.env.ERROR_SERVICE_API_KEY");
const withKeys = defineMap(
telemetryDefineArgs({
TELEMETRY_SERVICE_API_KEY: "tk",
ERROR_SERVICE_API_KEY: "ek",
}),
);
expect(withKeys["process.env.TELEMETRY_SERVICE_API_KEY"]).toBe('"tk"');
expect(withKeys["process.env.ERROR_SERVICE_API_KEY"]).toBe('"ek"');
});
it("JSON-escapes values so headers with quotes survive the define", () => {
const defines = defineMap(
telemetryDefineArgs({
OTEL_EXPORTER_OTLP_HEADERS: 'x-quote="quoted",y=2',
}),
);
expect(defines["process.env.OTEL_EXPORTER_OTLP_HEADERS"]).toBe(
JSON.stringify('x-quote="quoted",y=2'),
);
expect(JSON.parse(defines["process.env.OTEL_EXPORTER_OTLP_HEADERS"])).toBe(
'x-quote="quoted",y=2',
);
});
});
@@ -0,0 +1,55 @@
/**
* Build-time inlining of the telemetry configuration, mirroring the CLI
* bundle (`apps/cli/bun.mts`). `getTelemetryBuildTimeConfig` in
* `@cline/shared` reads `process.env.OTEL_*` at runtime, which is always
* empty for a packaged app launched from Finder/the Dock — so the packaged
* sidecar binary must have these values substituted into the bundle at
* build time. Vars that are unset at build time inline as "" (telemetry
* stays disabled), which keeps local/dev builds opted out without any
* keys living in the repo.
*/
/** Inlined only when present so secrets never inline as empty markers. */
const OPTIONAL_SECRET_ENV_VARS = [
"TELEMETRY_SERVICE_API_KEY",
"ERROR_SERVICE_API_KEY",
] as const;
/**
* Every env var `getTelemetryBuildTimeConfig` reads
* (sdk/packages/shared/src/services/telemetry-config.ts). Always inlined,
* defaulting to "", so a packaged binary never falls back to runtime env.
*/
const OTEL_ENV_VARS = [
"OTEL_TELEMETRY_ENABLED",
"OTEL_METRICS_EXPORTER",
"OTEL_LOGS_EXPORTER",
"OTEL_TRACES_EXPORTER",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_METRIC_EXPORT_INTERVAL",
] as const;
/**
* `--define` argument pairs for `bun build`, e.g.
* `["--define", 'process.env.OTEL_TELEMETRY_ENABLED="1"', ...]`.
*/
export function telemetryDefineArgs(
env: Record<string, string | undefined> = process.env,
): string[] {
const args: string[] = [];
const define = (name: string, value: string) => {
args.push("--define", `process.env.${name}=${JSON.stringify(value)}`);
};
for (const name of OPTIONAL_SECRET_ENV_VARS) {
const value = env[name];
if (value) {
define(name, value);
}
}
for (const name of OTEL_ENV_VARS) {
define(name, env[name] ?? "");
}
return args;
}
+26 -1
View File
@@ -1,5 +1,8 @@
import { homedir } from "node:os";
import { setHomeDirIfUnset } from "@cline/core";
import {
createClineTelemetryServiceConfig,
setHomeDirIfUnset,
} from "@cline/core";
import { captureSdkError, claimHubDaemonProcess } from "@cline/shared";
import { prewarmWorkspaceMetadata } from "./chat-session";
import { configureConnectorCliLaunch } from "./connectors";
@@ -12,6 +15,7 @@ import { createDesktopObservability } from "./observability";
import { resolveWorkspaceRoot } from "./paths";
import { startServer } from "./server";
import { ensureLoginShellPath } from "./shell-path";
import { buildTelemetrySelfcheckReport } from "./telemetry-selfcheck";
import { BunRuntime, SIDECAR_HOST, SIDECAR_MODE, SIDECAR_PORT } from "./types";
const SHUTDOWN_TIMEOUT_MS = 5_000;
@@ -143,7 +147,28 @@ async function main() {
);
}
/**
* Prints whether the telemetry configuration that was inlined at build time
* (see scripts/telemetry-define-args.ts) actually made it into this binary,
* then exits. CI runs this against the packaged sidecar and fails the
* publish when a release-grade build reports `"enabled":false` or an
* unusable OTLP endpoint, so a regression in the build-time inlining can
* never ship silently again.
*/
function runTelemetrySelfcheck(): void {
const report = buildTelemetrySelfcheckReport(
createClineTelemetryServiceConfig(),
);
process.stdout.write(`${JSON.stringify(report)}\n`);
}
async function runEntrypoint(): Promise<void> {
// Before the daemon-sentinel claim: the selfcheck only inspects build-time
// config and must not consume the sentinel or start anything.
if (process.argv.includes("--telemetry-selfcheck")) {
runTelemetrySelfcheck();
return;
}
// Claim rather than read: consuming the sentinel keeps daemon-hosted sessions
// from handing it to every process they spawn.
if (claimHubDaemonProcess()) {
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { buildTelemetrySelfcheckReport } from "./telemetry-selfcheck";
describe("buildTelemetrySelfcheckReport", () => {
it("reports the endpoint host for usable http(s) endpoints", () => {
expect(
buildTelemetrySelfcheckReport({
enabled: true,
otlpEndpoint: "https://otel.example.com:4318",
logsExporter: "otlp",
metricsExporter: "otlp",
}),
).toEqual({
telemetry_selfcheck: true,
enabled: true,
otlp_endpoint_host: "otel.example.com:4318",
logs_exporter: "otlp",
metrics_exporter: "otlp",
});
expect(
buildTelemetrySelfcheckReport({
enabled: true,
otlpEndpoint: "http://127.0.0.1:4319",
}).otlp_endpoint_host,
).toBe("127.0.0.1:4319");
});
it("reports an empty host when no endpoint was inlined", () => {
expect(
buildTelemetrySelfcheckReport({ enabled: false }).otlp_endpoint_host,
).toBe("");
expect(
buildTelemetrySelfcheckReport({ enabled: true, otlpEndpoint: "" })
.otlp_endpoint_host,
).toBe("");
});
it("flags unparseable endpoints", () => {
expect(
buildTelemetrySelfcheckReport({
enabled: true,
otlpEndpoint: "not a url at all",
}).otlp_endpoint_host,
).toBe("invalid-endpoint-url");
});
it("flags endpoints the OTLP http exporters cannot speak", () => {
for (const otlpEndpoint of [
"ftp://collector.example.com:4318",
"grpc://collector.example.com:4317",
"file:///tmp/otel",
]) {
expect(
buildTelemetrySelfcheckReport({ enabled: true, otlpEndpoint })
.otlp_endpoint_host,
).toBe("invalid-endpoint-url");
}
});
it("only treats an explicit true as enabled", () => {
expect(buildTelemetrySelfcheckReport({}).enabled).toBe(false);
expect(buildTelemetrySelfcheckReport({ enabled: true }).enabled).toBe(true);
});
});
@@ -0,0 +1,58 @@
/**
* Builds the `--telemetry-selfcheck` report (see index.ts). CI runs the
* packaged sidecar with that flag and fails the publish unless the report
* shows telemetry enabled with a usable OTLP endpoint, so the checks here
* define what a release-grade build must prove.
*/
export type TelemetrySelfcheckConfig = {
enabled?: boolean;
otlpEndpoint?: string;
logsExporter?: string;
metricsExporter?: string;
};
export type TelemetrySelfcheckReport = {
telemetry_selfcheck: true;
enabled: boolean;
/**
* The OTLP endpoint host when the endpoint is a usable http(s) URL,
* `""` when no endpoint was inlined, or `"invalid-endpoint-url"` when
* the endpoint cannot work at runtime (unparseable, hostless, or a
* non-http scheme the OTLP http/json exporters cannot speak).
*/
otlp_endpoint_host: string;
logs_exporter: string;
metrics_exporter: string;
};
// The SDK's OTLP exporters are the http/json ones
// (@opentelemetry/exporter-*-otlp-http); any other scheme would be accepted
// here only to fail at runtime.
const USABLE_OTLP_PROTOCOLS = new Set(["http:", "https:"]);
function otlpEndpointHost(otlpEndpoint: string | undefined): string {
if (!otlpEndpoint) {
return "";
}
try {
const url = new URL(otlpEndpoint);
return USABLE_OTLP_PROTOCOLS.has(url.protocol) && url.host
? url.host
: "invalid-endpoint-url";
} catch {
return "invalid-endpoint-url";
}
}
export function buildTelemetrySelfcheckReport(
config: TelemetrySelfcheckConfig,
): TelemetrySelfcheckReport {
return {
telemetry_selfcheck: true,
enabled: config.enabled === true,
otlp_endpoint_host: otlpEndpointHost(config.otlpEndpoint),
logs_exporter: config.logsExporter ?? "",
metrics_exporter: config.metricsExporter ?? "",
};
}