@@ -3,6 +3,7 @@ import { join } from "path";
33import { getConfigDir , ensureConfigDir } from "../config/paths.ts" ;
44import { buildRemoteAemOptions , type TrackingEvent } from "./event.ts" ;
55import { detectEnv } from "./env.ts" ;
6+ import { ensureUid , loadUid } from "./uid.ts" ;
67import Tracker from "../../lib/remote-telemetry/tracker.cjs" ;
78import EventPlugin from "../../lib/remote-telemetry/event-plugin.cjs" ;
89
@@ -12,74 +13,90 @@ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
1213
1314let remoteSendEvent : ( ( name : string , opts : Record < string , unknown > ) => void ) | undefined ;
1415
15- // 追踪在途( in-flight)的远端发送请求,以便在进程退出前等待它们完成。
16- // 底层 tracker 是 fire-and-forget 模式(发起 fetch 后不再持有 promise);
17- // 如果不追踪,CLI 在短命令或 SIGINT 场景下会在请求真正落网络前就退出。
18- // 配合 tracker.js 中的 `keepalive: true`,await 这些 promise 可以在不阻塞
19- // 主流程的前提下,尽最大努力把埋点送达。
16+ // Track in-flight remote sends so we can wait for them before process exit.
17+ // The underlying tracker is fire-and-forget ( fetch starts without holding the promise);
18+ // without tracking, short commands or SIGINT can exit before the request hits the network.
19+ // With `keepalive: true` in tracker.js, awaiting these promises best-effort delivers
20+ // telemetry without blocking the main command path.
2021const SEND_TIMEOUT_MS = 1000 ;
2122interface PendingSend {
2223 controller : AbortController ;
2324 promise : Promise < void > ;
2425}
2526const inflightSends = new Set < PendingSend > ( ) ;
2627let remoteClient : any = undefined ;
28+ let remoteInitialized = false ;
2729
28- try {
29- // env 传给 Tracker 顶层 config 后会进 URL 查询参数, AEM 后台 "环境" 列
30- // (原始字段 env) 直接读它, 值域 prod/pre/daily/dev。我们目前只用 prod/dev。
31- const client = new Tracker ( { pid : "bailian-cli-node" , env : detectEnv ( ) } ) ;
32- // 仅对当前实例的 `send` 做猴补丁,把每次 fetch 的 promise 收集起来。
33- // 不修改 prototype,避免污染其他可能存在的 Tracker 使用方。
34- const originalSend = client . send . bind ( client ) ;
35- client . send = function ( payload : string ) {
36- const controller = new AbortController ( ) ;
37- const deadline = setTimeout ( ( ) => controller . abort ( ) , SEND_TIMEOUT_MS ) ;
38- deadline . unref ?.( ) ;
39- const pending : PendingSend = {
40- controller,
41- promise : Promise . resolve ( )
42- . then ( ( ) => originalSend ( payload , { signal : controller . signal } ) )
43- . catch ( ( ) => { } )
44- . finally ( ( ) => {
45- // Bun may resolve body.cancel() before stopping its underlying fetch.
46- // Finish this send's private scope before removing its deadline.
47- controller . abort ( ) ;
48- clearTimeout ( deadline ) ;
49- inflightSends . delete ( pending ) ;
50- } ) ,
30+ /**
31+ * Construct the tracker on first remote report so disabled telemetry neither
32+ * constructs the tracker nor creates the uid file. Fully synchronous; see ensureUid.
33+ */
34+ function initRemote ( ) : void {
35+ if ( remoteInitialized ) return ;
36+ remoteInitialized = true ;
37+ try {
38+ const uid = ensureUid ( ) ;
39+ // `env` on the Tracker top-level config becomes a URL query param; AEM's
40+ // "environment" column (raw field `env`) reads it. Allowed values:
41+ // prod/pre/daily/dev. We currently use only prod/dev.
42+ // anonymous install id as `uid`, which AEM exposes as a dashboard field.
43+ const client = new Tracker ( {
44+ pid : "bailian-cli-node" ,
45+ env : detectEnv ( ) ,
46+ ...( uid ? { uid } : { } ) ,
47+ } ) ;
48+ // Monkey-patch only this instance's `send` to collect each fetch promise.
49+ // Do not touch the prototype so other Tracker consumers stay unaffected.
50+ const originalSend = client . send . bind ( client ) ;
51+ client . send = function ( payload : string ) {
52+ const controller = new AbortController ( ) ;
53+ const deadline = setTimeout ( ( ) => controller . abort ( ) , SEND_TIMEOUT_MS ) ;
54+ deadline . unref ?.( ) ;
55+ const pending : PendingSend = {
56+ controller,
57+ promise : Promise . resolve ( )
58+ . then ( ( ) => originalSend ( payload , { signal : controller . signal } ) )
59+ . catch ( ( ) => { } )
60+ . finally ( ( ) => {
61+ // Bun may resolve body.cancel() before stopping its underlying fetch.
62+ // Finish this send's private scope before removing its deadline.
63+ controller . abort ( ) ;
64+ clearTimeout ( deadline ) ;
65+ inflightSends . delete ( pending ) ;
66+ } ) ,
67+ } ;
68+ inflightSends . add ( pending ) ;
69+ return pending . promise ;
5170 } ;
52- inflightSends . add ( pending ) ;
53- return pending . promise ;
54- } ;
55- remoteClient = client ;
56- remoteSendEvent = client . use ( EventPlugin ) ;
57- } catch {
58- // 埋点逻辑任何异常都不能影响 CLI 主流程
71+ remoteClient = client ;
72+ remoteSendEvent = client . use ( EventPlugin ) ;
73+ } catch {
74+ // Telemetry must never affect the CLI main flow
75+ }
5976}
6077
6178/**
62- * 尽力等待所有在途的埋点发送完成(best -effort)。
79+ * Best -effort wait for all in-flight telemetry sends.
6380 *
64- * 1. 先调用 `_sendAll` 排空 tracker 内部的去抖队列,把还卡在 500ms 合并窗口里
65- * 的事件立刻推上网络。
66- * 2. 在预算内等待发送,预算耗尽时真正取消 DNS/HTTP/body 工作。
81+ * 1. Call `_sendAll` to drain the tracker's debounce queue so events still in
82+ * the 500ms merge window are pushed to the network immediately.
83+ * 2. Wait for sends within the budget; on timeout, cancel DNS/HTTP/body work.
6784 *
68- * 埋点永远不应阻塞 CLI:调用方应传入较短的超时(例如 1000ms),并始终与超时
69- * race。错误与超时一律静默吞掉。
85+ * Telemetry must never block the CLI: callers should pass a short timeout
86+ * (e.g. 1000ms) and always race against it. Errors and timeouts are swallowed.
7087 */
7188export async function flushTelemetry ( timeoutMs = 1000 ) : Promise < void > {
7289 let deadline : ReturnType < typeof setTimeout > | undefined ;
7390 try {
7491 if ( remoteClient ) {
7592 try {
76- // 排空去抖队列:把卡在 500ms 合并窗口里的事件立刻发出去
77- // (每次发出都会被追加到 inflightSends 里)。
93+ // Drain the debounce queue so events stuck in the 500ms merge window
94+ // are sent immediately (each send is appended to inflightSends).
7895 if ( typeof remoteClient . _sendAll === "function" ) {
7996 remoteClient . _sendAll ( ) ;
8097 }
8198 } catch {
82- // flush 自身绝不抛出
99+ // flush itself must never throw
83100 }
84101 }
85102 if ( inflightSends . size === 0 ) return ;
@@ -94,7 +111,7 @@ export async function flushTelemetry(timeoutMs = 1000): Promise<void> {
94111 } ) ,
95112 ] ) ;
96113 } catch {
97- // 埋点逻辑任何异常都不能影响 CLI 主流程
114+ // Telemetry must never affect the CLI main flow
98115 } finally {
99116 clearTimeout ( deadline ) ;
100117 }
@@ -111,20 +128,25 @@ export async function localSink(event: TrackingEvent): Promise<void> {
111128 unlinkSync ( path ) ;
112129 }
113130 } catch {
114- // 文件还不存在,忽略
131+ // File does not exist yet; ignore
115132 }
116133
117- appendFileSync ( path , JSON . stringify ( event ) + "\n" , { mode : 0o600 } ) ;
134+ // Read-only; remoteSink creates the file on first report.
135+ const uid = loadUid ( ) ;
136+ appendFileSync ( path , JSON . stringify ( uid ? { ...event , uid } : event ) + "\n" , {
137+ mode : 0o600 ,
138+ } ) ;
118139 } catch {
119- // 埋点逻辑任何异常都不能影响 CLI 主流程
140+ // Telemetry must never affect the CLI main flow
120141 }
121142}
122143
123144export async function remoteSink ( event : TrackingEvent ) : Promise < void > {
124145 try {
146+ initRemote ( ) ;
125147 if ( ! remoteSendEvent ) return ;
126148 remoteSendEvent ( event . command , buildRemoteAemOptions ( event ) ) ;
127149 } catch {
128- // 埋点逻辑任何异常都不能影响 CLI 主流程
150+ // Telemetry must never affect the CLI main flow
129151 }
130152}
0 commit comments