Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion plugins/trace-codex/src/agents/codex/event-processor.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { describe, expect, test } from "bun:test";
import { afterEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ReportingConfig, SpanFactory } from "../../braintrust/logger.ts";
import type { EnqueueEvent } from "../../server/routes.ts";
import { createTestLogger, spansToTree, withCapturedTrace } from "../../test-helpers.ts";
Expand Down Expand Up @@ -41,6 +45,32 @@ import type {
WaitForOptions,
} from "./transcript-reader.ts";

const tempDirs: string[] = [];

afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) rmSync(dir, { recursive: true, force: true });
}
});

function makeGitRepo(): { dir: string; commit: string } {
const dir = mkdtempSync(join(tmpdir(), "codex-git-metadata-"));
tempDirs.push(dir);
execFileSync("git", ["init"], { cwd: dir, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir });
execFileSync("git", ["config", "user.name", "Test User"], { cwd: dir });
writeFileSync(join(dir, "README.md"), "hello\n");
execFileSync("git", ["add", "README.md"], { cwd: dir });
execFileSync("git", ["commit", "-m", "init"], { cwd: dir, stdio: "ignore" });
execFileSync("git", ["branch", "-M", "main"], { cwd: dir });
execFileSync("git", ["remote", "add", "origin", "https://token@github.com/acme/app.git"], {
cwd: dir,
});
const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, encoding: "utf8" }).trim();
return { dir, commit };
}

// Helpers below build a session as an ordered list of transcript writes and
// hooks. The final `stop()` hook triggers the catch-up that turns the buffered
// transcript records into spans (waiting for the task_complete sentinel).
Expand Down Expand Up @@ -107,6 +137,20 @@ describe("CodexEventProcessor: root span", () => {
);
});

test("root span metadata includes minimal git attribution fields", async () => {
const repo = makeGitRepo();

await assertProducesTrace([sessionStart(), sessionMeta({ cwd: repo.dir }), stop({})], {
span_attributes: { type: "task" },
metadata: {
git_origin_url: "https://github.com/acme/app.git",
git_branch: "main",
git_commit_sha: repo.commit,
},
children: [],
});
});

test("duplicate session_meta still yields a single root span", async () => {
await assertProducesTrace(
[sessionStart(), sessionMeta({ cwd: "/work" }), sessionMeta({ cwd: "/work" }), stop({})],
Expand Down
3 changes: 3 additions & 0 deletions plugins/trace-codex/src/agents/codex/event-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
type SpanRef,
spanRef,
} from "../../braintrust/logger.ts";
import { gitMetadataForCwd } from "../../git-metadata.ts";
import type { Logger } from "../../log.ts";
import type { EventProcessor } from "../../processor/event-processor.ts";
import type { EnqueueEvent } from "../../server/routes.ts";
Expand Down Expand Up @@ -1154,6 +1155,7 @@ export class CodexEventProcessor implements EventProcessor {
const cwd = typeof payload.cwd === "string" ? payload.cwd : undefined;
const sessionId = typeof payload.id === "string" ? payload.id : this.queueId;
const startTime = isoToUnixSeconds(record.timestamp);
const gitMetadata = gitMetadataForCwd(cwd);

const projectDir = projectDirName(cwd);
const spanName = projectDir ? `codex: ${projectDir}` : "codex session";
Expand All @@ -1172,6 +1174,7 @@ export class CodexEventProcessor implements EventProcessor {
session_id: sessionId,
model: this.mainScope?.model,
cwd,
...gitMetadata,
source: this.rootEnrichment.source,
permission_mode: this.rootEnrichment.permissionMode,
cli_version:
Expand Down
73 changes: 73 additions & 0 deletions plugins/trace-codex/src/git-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { afterEach, describe, expect, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { gitMetadataForCwd, redactGitRemoteUrl } from "./git-metadata.ts";

const tempDirs: string[] = [];

afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) rmSync(dir, { recursive: true, force: true });
}
});

function makeTempDir(prefix: string): string {
const dir = mkdtempSync(join(tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}

function makeGitRepo(): { dir: string; commit: string } {
const dir = makeTempDir("codex-git-metadata-");
execFileSync("git", ["init"], { cwd: dir, stdio: "ignore" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir });
execFileSync("git", ["config", "user.name", "Test User"], { cwd: dir });
writeFileSync(join(dir, "README.md"), "hello\n");
execFileSync("git", ["add", "README.md"], { cwd: dir });
execFileSync("git", ["commit", "-m", "init"], { cwd: dir, stdio: "ignore" });
execFileSync("git", ["branch", "-M", "main"], { cwd: dir });
execFileSync("git", ["remote", "add", "origin", "https://token@github.com/acme/app.git"], {
cwd: dir,
});
const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, encoding: "utf8" }).trim();
return { dir, commit };
}

describe("git metadata", () => {
test("redacts credentials from URL-style remotes", () => {
expect(redactGitRemoteUrl("https://token:secret@github.com/acme/app.git")).toBe(
"https://github.com/acme/app.git",
);
});

test("keeps scp-like SSH remotes intact", () => {
expect(redactGitRemoteUrl("git@github.com:acme/app.git")).toBe("git@github.com:acme/app.git");
});

test("captures origin, branch, and commit", () => {
const repo = makeGitRepo();

expect(gitMetadataForCwd(repo.dir)).toEqual({
git_origin_url: "https://github.com/acme/app.git",
git_branch: "main",
git_commit_sha: repo.commit,
});
});

test("omits branch for detached HEAD", () => {
const repo = makeGitRepo();
execFileSync("git", ["checkout", "--detach", "HEAD"], { cwd: repo.dir, stdio: "ignore" });

expect(gitMetadataForCwd(repo.dir)).toEqual({
git_origin_url: "https://github.com/acme/app.git",
git_commit_sha: repo.commit,
});
});

test("omits all fields outside a git repo", () => {
expect(gitMetadataForCwd(makeTempDir("codex-not-git-"))).toEqual({});
});
});
53 changes: 53 additions & 0 deletions plugins/trace-codex/src/git-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { spawnSync } from "node:child_process";

export interface GitMetadata {
git_origin_url?: string;
git_branch?: string;
git_commit_sha?: string;
}

function runGit(cwd: string, args: string[]): string | undefined {
try {
const result = spawnSync("git", ["-C", cwd, ...args], {
encoding: "utf8",
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
timeout: 500,
windowsHide: true,
});
if (result.status !== 0 || typeof result.stdout !== "string") return undefined;
const value = result.stdout.trim();
return value.length > 0 ? value : undefined;
} catch {
return undefined;
}
}

export function redactGitRemoteUrl(remoteUrl: string): string {
const trimmed = remoteUrl.trim();
if (!trimmed) return trimmed;

try {
const parsed = new URL(trimmed);
if (parsed.username || parsed.password) {
parsed.username = "";
parsed.password = "";
}
return parsed.toString();
} catch {
return trimmed;
}
}

export function gitMetadataForCwd(cwd: string | undefined): GitMetadata {
if (!cwd) return {};

const origin = runGit(cwd, ["remote", "get-url", "origin"]);
const branch = runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
const commit = runGit(cwd, ["rev-parse", "HEAD"]);

return {
...(origin ? { git_origin_url: redactGitRemoteUrl(origin) } : {}),
...(branch ? { git_branch: branch } : {}),
...(commit ? { git_commit_sha: commit } : {}),
};
}
Loading