diff --git a/src/cli.ts b/src/cli.ts index b54e664..3fc387a 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -63,10 +63,12 @@ async function main() { await installGit(); // Initialize error tracking and telemetry (no-ops if opted out). - // Telemetry resolves repo context asynchronously — we don't block on it; - // any events emitted before it finishes simply won't carry repo_id. + // Await telemetry so the repo context is resolved before the preAction + // hook fires `command_executed` — otherwise that event always lands + // without `repo_id`. The repo lookup is a few cached git subprocesses + // (~5–20ms on a cold run) and runs exactly once per invocation. initSentry(); - void initTelemetry(); + await initTelemetry(); const logLevelOption = new Option("--log-level ", "Set log verbosity") .choices(["error", "warn", "info", "debug"] as const) diff --git a/src/helpers/repo.ts b/src/helpers/repo.ts index 0c63b5e..51cf769 100644 --- a/src/helpers/repo.ts +++ b/src/helpers/repo.ts @@ -105,11 +105,13 @@ export async function getRepoContext(): Promise { return cached; } - const remoteUrl = await runGitCapture( - ["config", "--get", "remote.origin.url"], - cwd - ); - const defaultBranch = await resolveDefaultBranch(cwd); + // Run the remaining probes concurrently — they're independent and the + // dominant cost on Windows is serial subprocess spawn (~25ms each), not + // the git work itself. + const [remoteUrl, defaultBranch] = await Promise.all([ + runGitCapture(["config", "--get", "remote.origin.url"], cwd), + resolveDefaultBranch(cwd), + ]); if (!remoteUrl) { cached = { @@ -301,18 +303,21 @@ async function runGitCheck(args: string[], cwd: string): Promise { } async function resolveDefaultBranch(cwd: string): Promise { - // Prefer the remote HEAD symbolic ref (e.g., `origin/main`) - const symRef = await runGitCapture( - ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], - cwd - ); + // Fire the preferred lookup (remote HEAD symbolic ref) and the fallback + // (currently-checked-out branch) in parallel. The fallback subprocess is + // cheap and lets us hide its spawn cost behind the other concurrent git + // calls — serial, it was the tail on the "no remote HEAD" path. + const [symRef, currentBranch] = await Promise.all([ + runGitCapture(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd), + runGitCapture(["rev-parse", "--abbrev-ref", "HEAD"], cwd), + ]); if (symRef) { const slash = symRef.indexOf("/"); return slash >= 0 ? symRef.slice(slash + 1) : symRef; } - // Fallback: whatever branch is currently checked out. Not strictly the - // "default" branch, but better than null for a single-user local repo. - return runGitCapture(["rev-parse", "--abbrev-ref", "HEAD"], cwd); + // Fallback: not strictly the "default" branch, but better than null for + // a single-user local repo. + return currentBranch; } // --------------------------------------------------------------------------- diff --git a/src/helpers/telemetry.ts b/src/helpers/telemetry.ts index 70ec74c..9673033 100644 --- a/src/helpers/telemetry.ts +++ b/src/helpers/telemetry.ts @@ -139,8 +139,10 @@ function getCommonProperties(): Record { * If telemetry is disabled, this is a no-op and all subsequent calls are too. * * Returns a promise that resolves once the async repo-context lookup is done. - * Callers can safely `trackEvent()` before awaiting — events emitted during - * the window before repo resolution just won't include `repo_id` / `repo_host`. + * Callers should `await` before emitting events so every event carries + * `repo_id` / `repo_host` — emitting before the await resolves means the + * event ships without repo identity. The repo lookup runs a handful of git + * subprocesses (cached per-process), so the added startup latency is small. */ export function initTelemetry(): Promise { if (!isTelemetryEnabled()) {