diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1905c6be..26781ede1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,6 +84,10 @@ jobs: OPENCODE_CHANNEL: latest GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Embedded into the binary by build.ts via Bun `define`. Read at + # runtime in @browser-use/bcode-browser/telemetry, gated by + # DO_NOT_TRACK and any user-supplied LMNR_PROJECT_API_KEY. + BCODE_DEFAULT_LMNR_KEY: ${{ secrets.LMNR_PROJECT_API_KEY_OSS }} run: | ./packages/opencode/script/build.ts diff --git a/packages/bcode-browser/src/telemetry.ts b/packages/bcode-browser/src/telemetry.ts new file mode 100644 index 000000000..fcb078d16 --- /dev/null +++ b/packages/bcode-browser/src/telemetry.ts @@ -0,0 +1,45 @@ +// Telemetry key injection. +// +// At build time, `packages/opencode/script/build.ts` substitutes +// `BCODE_DEFAULT_LMNR_KEY` with a string literal via Bun's `define`. The +// release workflow sources that value from a GitHub Actions secret; local +// `bun run build` invocations leave it empty, so self-builds never emit +// telemetry. +// +// At runtime we set `LMNR_PROJECT_API_KEY` from the embedded default if and +// only if: +// - DO_NOT_TRACK is not set (any non-empty value opts out — DO_NOT_TRACK +// standard convention), AND +// - LMNR_PROJECT_API_KEY is not already set in the environment (BYO key +// wins; explicit empty string is respected as "no key please"), AND +// - the embedded default is non-empty. +// +// `applyTelemetryKey()` is invoked as a side effect on module import (last +// statement of this file). Because `packages/opencode/src/index.ts` imports +// this module before any other module that might consume the env var, the +// gate is guaranteed to run before any downstream module-load code can +// observe `LMNR_PROJECT_API_KEY` — sidestepping ESM static-import hoisting +// entirely. + +declare const BCODE_DEFAULT_LMNR_KEY: string + +export const applyTelemetryKey = () => { + // DO_NOT_TRACK: presence with any non-empty value opts out, per the + // de-facto standard (consoledonottrack.com, Astro, Homebrew, npm). + if (process.env.DO_NOT_TRACK) return + // LMNR_PROJECT_API_KEY: presence (not truthiness) wins so users who + // explicitly set it to an empty string get exactly that — no key. + if (process.env.LMNR_PROJECT_API_KEY !== undefined) return + // `typeof` first: in dev (no Bun `define` substitution) the identifier is + // undeclared and a direct read throws ReferenceError. + if (typeof BCODE_DEFAULT_LMNR_KEY === "undefined" || !BCODE_DEFAULT_LMNR_KEY) return + process.env.LMNR_PROJECT_API_KEY = BCODE_DEFAULT_LMNR_KEY +} + +// Run as an import side effect: this module is imported as the very first +// import of `packages/opencode/src/index.ts`, so by the time any other +// module's top-level code reads `LMNR_PROJECT_API_KEY` the gate has already +// resolved. +applyTelemetryKey() + +export * as Telemetry from "./telemetry" diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 8f8b224f4..d89972744 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -235,6 +235,10 @@ for (const item of targets) { OPENCODE_WORKER_PATH: workerPath, OPENCODE_CHANNEL: `'${Script.channel}'`, OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", + // Build-time-embedded Laminar project key. Populated by release CI from + // the LMNR_PROJECT_API_KEY_OSS secret; empty for local builds. Runtime + // use is gated in @browser-use/bcode-browser/src/telemetry.ts. + BCODE_DEFAULT_LMNR_KEY: JSON.stringify(process.env.BCODE_DEFAULT_LMNR_KEY ?? ""), }, }) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 86bda3960..272df358a 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -1,3 +1,9 @@ +// Telemetry key injection runs as an import side effect of this module, +// before any subsequent import is evaluated. Keep this as the FIRST import +// so the LMNR_PROJECT_API_KEY env var is settled before any downstream +// module-load code reads it. +import "@browser-use/bcode-browser/telemetry" + import yargs from "yargs" import { hideBin } from "yargs/helpers" import { RunCommand } from "./cli/cmd/run"