From 8e4cae82e707635b4403b2d7c273c2bbe3643a07 Mon Sep 17 00:00:00 2001 From: rendianmeng Date: Thu, 12 Nov 2026 15:14:42 +0800 Subject: [PATCH] fix: report release binary telemetry as production Bun compile inlines NODE_ENV as development, so binary command events never reached the production dashboard. --- docs/agents/telemetry-change.md | 3 ++ packages/core/src/telemetry/env.ts | 20 ++++++---- packages/core/tests/telemetry-env.test.ts | 44 +++++++++++++++++++++ tools/release/lib/binary-compile.mjs | 47 +++++++++++++++-------- tools/release/lib/binary-compile.test.mjs | 18 +++++++++ 5 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 packages/core/tests/telemetry-env.test.ts create mode 100644 tools/release/lib/binary-compile.test.mjs diff --git a/docs/agents/telemetry-change.md b/docs/agents/telemetry-change.md index beda01591..f365e2e6a 100644 --- a/docs/agents/telemetry-change.md +++ b/docs/agents/telemetry-change.md @@ -121,6 +121,8 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网 1. 本地 `~/.bailian/telemetry.jsonl`:权限 `0600`,超过 5 MB 后重建 2. AEM:`pid=bailian-cli-node`,源码运行自动使用 `env=dev`,npm 安装或编译二进制使用 `env=prod` +编译二进制的 `env=prod` 由 `BAILIAN_COMPILED=1` 决定,并且必须先于 `NODE_ENV`。`bun build --compile`(当前 pin 1.3.14)会把点访问 `process.env.NODE_ENV` 内联成构建机的值,未设置时是 `"development"`;`tools/release/lib/binary-compile.mjs` 同时 `--define process.env.NODE_ENV="production"`。这两处读取必须写成 `process.env.*`,包进别的变量后 `--define` 匹配不到。 + 底层 Node tracker 还会附加公共设备字段:OS 类型/版本、Node 应用名与版本、平台,以及由本机网络标识计算的 MD5 `device_id`。 从 2.8.2 起,每条命令事件通过 `commandPrefix` 显式记录调用入口,三种入口仍共用 `pid=bailian-cli-node`: @@ -202,3 +204,4 @@ vp test packages/core/tests packages/runtime/tests/telemetry-stage.test.ts packa - ✗ 新增 bypass `fetch` 后漏掉应由网关消费的 source-config,或把它发给 OSS / npm / 第三方下载地址 - ✗ 只改 `bl` 入口,导致 `kscli` 的产品名或版本标签错误 - ✗ 把帮助、版本或参数校验失败算进“全部命令”;这些路径当前没有进入 telemetry middleware +- ✗ 把 `NODE_ENV` 判断放在 `BAILIAN_COMPILED` 前面。Bun 编译会把 `process.env.NODE_ENV` 折成 `"development"`,二进制流量全部进 `env=dev`,生产看板为空 diff --git a/packages/core/src/telemetry/env.ts b/packages/core/src/telemetry/env.ts index d2772510e..81d5e8308 100644 --- a/packages/core/src/telemetry/env.ts +++ b/packages/core/src/telemetry/env.ts @@ -1,26 +1,30 @@ /** - * 判断当前运行环境。任一条件为真即视为 dev,默认 prod。 + * 判断当前运行环境。默认 prod。 * - * 1. NODE_ENV=development — Node 圈通用约定,测试同学/CI 可显式声明 - * 2. Bun 编译二进制(BAILIAN_COMPILED=1)— 一律 prod + * 1. Bun 编译二进制(BAILIAN_COMPILED=1)— 一律 prod。必须先于 NODE_ENV: + * `bun build --compile` 会把 `process.env.NODE_ENV` 内联成构建机的值, + * 未设置时是 `"development"`。放在前面会让二进制流量全部记成 dev。 + * release 编译同时 `--define process.env.NODE_ENV="production"`。 + * 2. NODE_ENV=development — 仅非二进制。测试同学/CI 可显式声明 * 3. 当前模块文件路径不在 node_modules 里 — 自动识别从源码运行(pnpm dev / * npm link / 直接 pnpm -F bailian-cli exec tsx src/main.ts),避免开发者忘记设环境变量 * 时仍把数据打到 prod * - * 缓存结果,模块加载期算一次就行。 + * 这两处必须直接读取 `process.env.*`。包一层再读,Bun 的 `--define` 匹配不到, + * 编译进二进制的标记会失效。缓存结果,模块加载期算一次就行。 */ let cachedEnv: "dev" | "prod" | undefined; export function detectEnv(): "dev" | "prod" { if (cachedEnv) return cachedEnv; - if (process.env.NODE_ENV === "development") { - cachedEnv = "dev"; - return cachedEnv; - } if (process.env.BAILIAN_COMPILED === "1") { cachedEnv = "prod"; return cachedEnv; } + if (process.env.NODE_ENV === "development") { + cachedEnv = "dev"; + return cachedEnv; + } cachedEnv = import.meta.url.includes("/node_modules/") ? "prod" : "dev"; return cachedEnv; } diff --git a/packages/core/tests/telemetry-env.test.ts b/packages/core/tests/telemetry-env.test.ts new file mode 100644 index 000000000..9a18780c4 --- /dev/null +++ b/packages/core/tests/telemetry-env.test.ts @@ -0,0 +1,44 @@ +import { afterEach, expect, test, vi } from "vite-plus/test"; + +afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); +}); + +async function loadDetectEnv(): Promise<() => "dev" | "prod"> { + vi.resetModules(); + const telemetryEnv = await import("../src/telemetry/env.ts"); + return telemetryEnv.detectEnv; +} + +test("compiled binary reports prod even when NODE_ENV is development", async () => { + vi.stubEnv("BAILIAN_COMPILED", "1"); + vi.stubEnv("NODE_ENV", "development"); + const detectEnv = await loadDetectEnv(); + expect(detectEnv()).toBe("prod"); +}); + +test("compiled binary reports prod when NODE_ENV is unset", async () => { + vi.stubEnv("BAILIAN_COMPILED", "1"); + vi.stubEnv("NODE_ENV", undefined); + const detectEnv = await loadDetectEnv(); + expect(detectEnv()).toBe("prod"); +}); + +test("NODE_ENV=development reports dev outside a compiled binary", async () => { + vi.stubEnv("BAILIAN_COMPILED", undefined); + vi.stubEnv("NODE_ENV", "development"); + const detectEnv = await loadDetectEnv(); + expect(detectEnv()).toBe("dev"); +}); + +test("source tree reports dev when NODE_ENV is production or test", async () => { + vi.stubEnv("BAILIAN_COMPILED", undefined); + vi.stubEnv("NODE_ENV", "production"); + const productionDetectEnv = await loadDetectEnv(); + expect(productionDetectEnv()).toBe("dev"); + + vi.stubEnv("NODE_ENV", "test"); + const testDetectEnv = await loadDetectEnv(); + expect(testDetectEnv()).toBe("dev"); +}); diff --git a/tools/release/lib/binary-compile.mjs b/tools/release/lib/binary-compile.mjs index 59cf2f4f8..ccfe48323 100644 --- a/tools/release/lib/binary-compile.mjs +++ b/tools/release/lib/binary-compile.mjs @@ -9,6 +9,8 @@ */ import { existsSync } from "node:fs"; import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; function parseArgs(argv) { let entry = null; @@ -34,11 +36,14 @@ function parseArgs(argv) { return { entry, outfile, target }; } -const { entry, outfile, target } = parseArgs(process.argv.slice(2)); - -const result = spawnSync( - "bun", - [ +/** + * Bun ≤1.3.14 inlines `process.env.NODE_ENV` during `bun build --compile`. + * Unset on the build machine becomes the string `"development"`, which + * `detectEnv()` would report as dev. Pin production here, and keep the + * compiled-marker define so the runtime check still wins if that default changes. + */ +export function bunCompileArguments(entry, outfile, target) { + return [ "build", entry, "--compile", @@ -48,18 +53,30 @@ const result = spawnSync( target, "--define", 'process.env.BAILIAN_COMPILED="1"', - ], - { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }, -); + "--define", + 'process.env.NODE_ENV="production"', + ]; +} -if (result.stdout) process.stdout.write(result.stdout); -if (result.stderr) process.stderr.write(result.stderr); +function compileBinary({ entry, outfile, target }) { + const result = spawnSync("bun", bunCompileArguments(entry, outfile, target), { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); -if (result.status !== 0) { - process.exit(result.status ?? 1); + if (result.stdout) process.stdout.write(result.stdout); + if (result.stderr) process.stderr.write(result.stderr); + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + + if (!existsSync(outfile)) { + console.error(`bun build --compile exited 0 but outfile missing: ${outfile}`); + process.exit(1); + } } -if (!existsSync(outfile)) { - console.error(`bun build --compile exited 0 but outfile missing: ${outfile}`); - process.exit(1); +if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) { + compileBinary(parseArgs(process.argv.slice(2))); } diff --git a/tools/release/lib/binary-compile.test.mjs b/tools/release/lib/binary-compile.test.mjs new file mode 100644 index 000000000..83f865ca7 --- /dev/null +++ b/tools/release/lib/binary-compile.test.mjs @@ -0,0 +1,18 @@ +import { expect, test } from "vite-plus/test"; +import { bunCompileArguments } from "./binary-compile.mjs"; + +test("release compile pins the compiled marker and production NODE_ENV", () => { + expect(bunCompileArguments("entry.ts", "out/bl", "bun-linux-x64")).toEqual([ + "build", + "entry.ts", + "--compile", + "--outfile", + "out/bl", + "--target", + "bun-linux-x64", + "--define", + 'process.env.BAILIAN_COMPILED="1"', + "--define", + 'process.env.NODE_ENV="production"', + ]); +});