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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Run a Codex turn, then open your Langfuse project to see the trace.
| `LANGFUSE_SECRET_KEY` / `LANGFUSE_CODEX_SECRET_KEY` | Yes | — | Langfuse secret key (`sk-lf-...`) |
| `LANGFUSE_BASE_URL` / `LANGFUSE_CODEX_BASE_URL` | No | `https://cloud.langfuse.com` | Langfuse host / data region |
| `LANGFUSE_TRACING_ENVIRONMENT` / `LANGFUSE_CODEX_ENVIRONMENT` | No | — | Environment label for the traces (e.g. `production`) |
| `LANGFUSE_CODEX_USER_ID` | No | | Attach a user id to all traces |
| `LANGFUSE_CODEX_USER_ID` | No | Codex auth email, if found | Attach a user id to all traces |
| `LANGFUSE_CODEX_TAGS` | No | — | Tags for all traces (JSON array or comma-separated) |
| `LANGFUSE_CODEX_METADATA` | No | — | JSON object of metadata to attach to all traces |
| `LANGFUSE_CODEX_MAX_CHARS` | No | `20000` | Truncate inputs/outputs longer than this many characters |
Expand All @@ -115,7 +115,7 @@ Run a Codex turn, then open your Langfuse project to see the trace.
| `secret_key` | `LANGFUSE_SECRET_KEY` / `LANGFUSE_CODEX_SECRET_KEY` | — | Langfuse secret key |
| `base_url` | `LANGFUSE_BASE_URL` / `LANGFUSE_CODEX_BASE_URL` | `https://cloud.langfuse.com` | Langfuse host |
| `environment` | `LANGFUSE_TRACING_ENVIRONMENT` / `LANGFUSE_CODEX_ENVIRONMENT` | — | Environment label |
| `user_id` | `LANGFUSE_CODEX_USER_ID` | | User id for all traces |
| `user_id` | `LANGFUSE_CODEX_USER_ID` | Codex auth email, if found | User id for all traces |
| `tags` | `LANGFUSE_CODEX_TAGS` | — | Tags for all traces |
| `metadata` | `LANGFUSE_CODEX_METADATA` | — | Metadata object for all traces |
| `max_chars` | `LANGFUSE_CODEX_MAX_CHARS` | `20000` | Input/output truncation threshold |
Expand Down
31 changes: 31 additions & 0 deletions plugins/tracing/dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4286,6 +4286,7 @@ const DEFAULTS = {
debug: false,
fail_on_error: false
};
const CodexAuthSchema = object({ tokens: object({ id_token: string().optional() }).optional() }).passthrough();
function parseBoolean(value) {
if (typeof value === "boolean") return value;
if (typeof value !== "string") return void 0;
Expand Down Expand Up @@ -4353,6 +4354,30 @@ async function readConfigFile(file) {
return;
}
}
function readJwtPayload(token) {
const payload = token.split(".")[1];
if (!payload) return void 0;
try {
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
return parsed;
} catch {
return;
}
}
async function readCodexUserEmail(authFile) {
try {
const raw = JSON.parse(await fs.readFile(authFile, "utf-8"));
const token = CodexAuthSchema.parse(raw).tokens?.id_token;
if (!token) return void 0;
const email$1 = readJwtPayload(token)?.email;
if (typeof email$1 !== "string") return void 0;
const trimmed = email$1.trim();
return trimmed.length > 0 ? trimmed : void 0;
} catch {
return;
}
}
function getVar(suffix, env) {
return env[`LANGFUSE_CODEX_${suffix}`] ?? env[`LANGFUSE_${suffix}`];
}
Expand All @@ -4372,14 +4397,20 @@ function readEnvConfig(env) {
}));
}
const getHomeDir = () => process.env.HOME ?? os$2.homedir();
function getCodexAuthFile(home, env) {
const codexHome = env.CODEX_HOME?.trim();
return codexHome ? path.join(codexHome, "auth.json") : path.join(home, ".codex", "auth.json");
}
async function getConfig(options) {
const home = options?.home ?? getHomeDir();
const cwd = options?.cwd ?? process.cwd();
const env = options?.env ?? process.env;
const [globalConfig$1, localConfig] = await Promise.all([readConfigFile(path.join(home, ".codex", "langfuse.json")), readConfigFile(path.join(cwd, ".codex", "langfuse.json"))]);
const envConfig = readEnvConfig(env);
const codexUserId = globalConfig$1?.user_id ?? localConfig?.user_id ?? envConfig.user_id ? void 0 : await readCodexUserEmail(getCodexAuthFile(home, env));
return ConfigSchema.parse({
...DEFAULTS,
...codexUserId ? { user_id: codexUserId } : {},
...globalConfig$1,
...localConfig,
...envConfig
Expand Down
50 changes: 50 additions & 0 deletions plugins/tracing/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ const DEFAULTS: Pick<Config, "enabled" | "base_url" | "max_chars" | "debug" | "f
fail_on_error: false,
};

const CodexAuthSchema = z
.object({
tokens: z
.object({
id_token: z.string().optional(),
})
.optional(),
})
.passthrough();

function parseBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") return value;
if (typeof value !== "string") return undefined;
Expand Down Expand Up @@ -129,6 +139,36 @@ async function readConfigFile(file: string): Promise<Partial<Config> | undefined
}
}

function readJwtPayload(token: string): Record<string, unknown> | undefined {
const payload = token.split(".")[1];
if (!payload) return undefined;

try {
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8")) as unknown;
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
return parsed as Record<string, unknown>;
} catch {
return undefined;
}
}

async function readCodexUserEmail(authFile: string): Promise<string | undefined> {
try {
const raw = JSON.parse(await fs.readFile(authFile, "utf-8")) as unknown;
const auth = CodexAuthSchema.parse(raw);
const token = auth.tokens?.id_token;
if (!token) return undefined;

const email = readJwtPayload(token)?.email;
if (typeof email !== "string") return undefined;

const trimmed = email.trim();
return trimmed.length > 0 ? trimmed : undefined;
} catch {
return undefined;
}
}

function getVar(suffix: string, env: Record<string, string | undefined>): string | undefined {
return env[`LANGFUSE_CODEX_${suffix}`] ?? env[`LANGFUSE_${suffix}`];
}
Expand All @@ -153,6 +193,11 @@ function readEnvConfig(env: Record<string, string | undefined>): Partial<Config>

const getHomeDir = () => process.env.HOME ?? os.homedir();

function getCodexAuthFile(home: string, env: Record<string, string | undefined>): string {
const codexHome = env.CODEX_HOME?.trim();
return codexHome ? path.join(codexHome, "auth.json") : path.join(home, ".codex", "auth.json");
}

export async function getConfig(options?: {
home?: string;
cwd?: string;
Expand All @@ -167,9 +212,14 @@ export async function getConfig(options?: {
readConfigFile(path.join(cwd, ".codex", "langfuse.json")),
]);
const envConfig = readEnvConfig(env);
const explicitUserId = globalConfig?.user_id ?? localConfig?.user_id ?? envConfig.user_id;
const codexUserId = explicitUserId
? undefined
: await readCodexUserEmail(getCodexAuthFile(home, env));

return ConfigSchema.parse({
...DEFAULTS,
...(codexUserId ? { user_id: codexUserId } : {}),
...globalConfig,
...localConfig,
...envConfig,
Expand Down
73 changes: 73 additions & 0 deletions plugins/tracing/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ function makeTmpHome(file?: { rel: string; contents: unknown }): string {
return dir;
}

function makeJwt(payload: unknown): string {
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
return `${encode({ alg: "none" })}.${encode(payload)}.`;
}

afterEach(() => {
while (tmpDirs.length) {
fs.rmSync(tmpDirs.pop()!, { recursive: true, force: true });
Expand Down Expand Up @@ -68,6 +73,74 @@ describe("getConfig", () => {
expect(config.secret_key).toBe("sk-standard");
});

it("uses the Codex auth email as the default user id when available", async () => {
const home = makeTmpHome({
rel: ".codex/auth.json",
contents: {
tokens: {
id_token: makeJwt({ email: " user@example.com " }),
},
},
});

const config = await getConfig({ home, cwd: emptyHome(), env: {} });

expect(config.user_id).toBe("user@example.com");
});

it("reads the Codex auth email from CODEX_HOME when set", async () => {
const codexHome = makeTmpHome({
rel: "auth.json",
contents: {
tokens: {
id_token: makeJwt({ email: "codex-home@example.com" }),
},
},
});

const config = await getConfig({
home: emptyHome(),
cwd: emptyHome(),
env: { CODEX_HOME: codexHome },
});

expect(config.user_id).toBe("codex-home@example.com");
});

it("ignores missing or malformed Codex auth email claims", async () => {
const home = makeTmpHome({
rel: ".codex/auth.json",
contents: {
tokens: {
id_token: makeJwt({ name: "Codex User" }),
},
},
});

const config = await getConfig({ home, cwd: emptyHome(), env: {} });

expect(config.user_id).toBeUndefined();
});

it("keeps explicit user id config ahead of the Codex auth email", async () => {
const home = makeTmpHome({
rel: ".codex/auth.json",
contents: {
tokens: {
id_token: makeJwt({ email: "codex@example.com" }),
},
},
});

const config = await getConfig({
home,
cwd: emptyHome(),
env: { LANGFUSE_CODEX_USER_ID: "configured-user" },
});

expect(config.user_id).toBe("configured-user");
});

it("parses tags (JSON array or comma-separated) and metadata JSON", async () => {
const jsonArray = await getConfig({
home: emptyHome(),
Expand Down
Loading