Telemetry
Tree-Shaking Telemetry
Production zero contract: release builds contain no telemetry SDK code, no PostHog endpoint, no OTLP URL.
Tree-Shaking Telemetry
The contract: a cargo build --release of any Rust element, and an astro build of Sky / a pnpm run --filter Output build of Output, must contain zero bytes of telemetry SDK code, no PostHog endpoint string, no OTLP collector URL, no posthog-js import.
The Three Gates
Every capture site is gated by three layers stacked from outside in:
- Build mode gate — compile-time. Drops the whole stack from release / production bundles.
- Master kill switch (
Capture) — runtime. Drops the runtime call regardless of build mode. Used for one-off airgapped runs. - Per-pipe toggle (
Report,OTLPEnabled) — runtime, per-pipe. Lets PostHog and Jaeger be flipped independently.
Production builds rely on (1) so (2) and (3) never even run.
Rust — cfg!(debug_assertions)
pub fn Fn(EventName: &str, Properties: Option<Vec<(&str, &str)>>) {
if !cfg!(debug_assertions) { return; }
if std::env::var("Capture").as_deref() == Ok("false") { return; }
// ... call PostHog ...
}cfg!(debug_assertions) is false in --release. LLVM constant-folds the if, drops the dead branch, and the posthog-rs calls underneath become unreachable. Linker GC then strips the symbol entirely.
Verify:
cargo build -p Mountain --release
strings Element/Mountain/Target/release/Mountain | grep -c "posthog\|/capture/\|i.posthog.com"
# 0TypeScript — process.env.NODE_ENV / import.meta.env.DEV
esbuild + Vite substitute these literally at build time when configured with define:
// esbuild.config.ts
{
define: {
"process.env.NODE_ENV": JSON.stringify(
process.env["NODE_ENV"] ?? "development",
),
},
}Then in code:
let Bridge: typeof import("./PostHogBridge.js") | undefined;
if (process.env.NODE_ENV !== "production") {
Bridge = await import("./PostHogBridge.js");
}
export const CaptureEvent = (
Name: string,
Properties: Record<string, unknown>,
): void => {
Bridge?.CaptureEvent(Name, Properties);
};In a production build esbuild rewrites process.env.NODE_ENV to "production", constant-folds to if (false), drops the await import(), and the chunk graph never sees PostHogBridge.js.
Verify:
NODE_ENV=production pnpm run --filter Cocoon build
grep -rn "i.posthog.com\|/batch/" Element/Cocoon/Target/
# 0 matchesAstro — import.meta.env.DEV
Vite replaces import.meta.env.DEV with false for astro build. Wrap every Sky bridge import in:
if (import.meta.env.DEV) {
const { default: InitTelemetry } = await import("./Telemetry/Bridge.js");
InitTelemetry();
}Verify:
pnpm run --filter Sky build
grep -rn "posthog\|otlp\|jaeger" Element/Sky/Target/Static/Application/
# 0 matchesWhat About the PostHog API Key?
The key is built into release builds only if a release path imports the bridge. The gate above means a clean release never sees the key. For the few release-mode events we deliberately keep (e.g. license activations), use a separate “anonymous-only” key in .env.Land.PostHog.Production, gated through a different module that does not import the dev bridge.
Smoke Test
sh Maintain/Test/TelemetryTreeShake.shBuilds Mountain in --release, Cocoon / Sky / Output in NODE_ENV=production, runs strings / grep / du -sh against the output, fails CI if any forbidden symbol is found.
