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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
.claude
plugins/tracing/dist
pnpm-lock.yaml
plugins/tracing/test/fixtures
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ Run a Codex turn, then open your Langfuse project to see the trace.
| `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 |
| `LANGFUSE_CODEX_DEBUG` | No | `false` | Set to `"true"` for verbose logging to stderr |
| `LANGFUSE_CODEX_FAIL_ON_ERROR` | No | `false` | Set to `"true"` to make hook upload errors fail the hook |

### Data regions

Expand All @@ -107,24 +108,29 @@ Run a Codex turn, then open your Langfuse project to see the trace.

## JSON config reference

| Config key | Environment variable | Default | Description |
| ------------- | ------------------------------------------------------------- | ---------------------------- | --------------------------------- |
| `enabled` | `TRACE_TO_LANGFUSE` | `false` | Enable tracing |
| `public_key` | `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_CODEX_PUBLIC_KEY` | — | Langfuse public key |
| `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 |
| `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 |
| `debug` | `LANGFUSE_CODEX_DEBUG` | `false` | Verbose logging |
| Config key | Environment variable | Default | Description |
| --------------- | ------------------------------------------------------------- | ---------------------------- | --------------------------------- |
| `enabled` | `TRACE_TO_LANGFUSE` | `false` | Enable tracing |
| `public_key` | `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_CODEX_PUBLIC_KEY` | — | Langfuse public key |
| `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 |
| `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 |
| `debug` | `LANGFUSE_CODEX_DEBUG` | `false` | Verbose logging |
| `fail_on_error` | `LANGFUSE_CODEX_FAIL_ON_ERROR` | `false` | Fail the hook on upload errors |

## Troubleshooting

- **No traces appear** — confirm `plugin_hooks = true`, the plugin is enabled in `config.toml`, and `TRACE_TO_LANGFUSE=true` is visible to the Codex process. Run with `LANGFUSE_CODEX_DEBUG=true` to log to stderr.
- **Authentication fails** — check that the public/secret keys are valid and that `LANGFUSE_BASE_URL` matches the region the keys belong to.
- **Traces land in the wrong project** — API keys are project-scoped in Langfuse; use the keys for the project you want.
- **Testing hook failures** — set `LANGFUSE_CODEX_FAIL_ON_ERROR=true` together with `LANGFUSE_CODEX_DEBUG=true` to make Codex report upload or flush errors instead of failing open.
- **Checking dedup sidecars** — successful uploads of completed turns are recorded next to the rollout as `<rollout>.jsonl.langfuse`. If a Stop hook reads the rollout before Codex has written the turn-completed marker, the trace may upload without a sidecar entry; the next Stop hook will finalize and mark it.
- **Verifying in Langfuse** — use `npx langfuse-cli api traces list --from-timestamp <recent ISO> --limit 10 --order-by timestamp.desc --fields core,metrics,observations --json` with credentials for the same project.
- **Sandboxed/network-restricted runs** — Codex sandbox or network policy can prevent exports from reaching Langfuse. Debug logging and fail-on-error mode are the quickest way to distinguish hook execution from network failure.
- **Self-hosting** — the TypeScript SDK requires Langfuse platform version >= 3.95.0.

## Data sent to Langfuse
Expand Down
23 changes: 17 additions & 6 deletions plugins/tracing/dist/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4275,14 +4275,16 @@ const ConfigSchema = object({
tags: array(string()).optional(),
metadata: record(string(), string()).optional(),
max_chars: number().int().positive(),
debug: boolean()
debug: boolean(),
fail_on_error: boolean()
});
const PartialConfigSchema = ConfigSchema.partial();
const DEFAULTS = {
enabled: false,
base_url: "https://cloud.langfuse.com",
max_chars: 2e4,
debug: false
debug: false,
fail_on_error: false
};
function parseBoolean(value) {
if (typeof value === "boolean") return value;
Expand Down Expand Up @@ -4344,7 +4346,8 @@ async function readConfigFile(file) {
tags: raw.tags != null ? parseTags(raw.tags) : void 0,
metadata: raw.metadata != null ? parseMetadata(raw.metadata) : void 0,
max_chars: raw.max_chars != null ? parseInteger(raw.max_chars) : void 0,
debug: raw.debug != null ? parseBoolean(raw.debug) : void 0
debug: raw.debug != null ? parseBoolean(raw.debug) : void 0,
fail_on_error: raw.fail_on_error != null ? parseBoolean(raw.fail_on_error) : void 0
}));
} catch {
return;
Expand All @@ -4364,7 +4367,8 @@ function readEnvConfig(env) {
tags: parseTags(env.LANGFUSE_CODEX_TAGS),
metadata: parseMetadata(env.LANGFUSE_CODEX_METADATA),
max_chars: parseInteger(env.LANGFUSE_CODEX_MAX_CHARS),
debug: parseBoolean(env.LANGFUSE_CODEX_DEBUG)
debug: parseBoolean(env.LANGFUSE_CODEX_DEBUG),
fail_on_error: parseBoolean(env.LANGFUSE_CODEX_FAIL_ON_ERROR)
}));
}
const getHomeDir = () => process.env.HOME ?? os$2.homedir();
Expand Down Expand Up @@ -46983,12 +46987,13 @@ async function convertRollout(rolloutFile, options) {
if (turn.completed && turn.turnId) {
uploaded.add(turn.turnId);
await markTurnUploaded(rolloutFile, turn.turnId);
}
} else if (turn.turnId) debugLog(`uploaded in-progress turn ${turn.turnId}; waiting for completion before sidecar mark`);
}
}

//#endregion
//#region src/index.ts
let failOnError = process.env.LANGFUSE_CODEX_FAIL_ON_ERROR === "true";
/**
* Entry point for the Codex `Stop` hook.
*
Expand All @@ -46997,7 +47002,9 @@ async function convertRollout(rolloutFile, options) {
* transcript into Langfuse traces.
*
* The hook fails open: any error is logged (in debug mode) and swallowed so a
* tracing problem never blocks the Codex session.
* tracing problem never blocks the Codex session. Set
* `LANGFUSE_CODEX_FAIL_ON_ERROR=true` while testing if you want Codex to report
* hook failures instead.
*/
async function runHook() {
let hookInput;
Expand All @@ -47008,6 +47015,7 @@ async function runHook() {
}
const config$1 = await getConfig();
setDebug(config$1.debug);
failOnError = config$1.fail_on_error;
if (!config$1.enabled) {
debugLog("tracing disabled (set TRACE_TO_LANGFUSE=true to enable)");
return;
Expand All @@ -47025,16 +47033,19 @@ async function runHook() {
await convertRollout(hookInput.transcript_path, { config: config$1 });
} catch (error) {
debugLog("failed to convert rollout:", error);
if (config$1.fail_on_error) throw error;
} finally {
try {
await instrumentation.shutdown();
} catch (error) {
debugLog("error during flush/shutdown:", error);
if (config$1.fail_on_error) throw error;
}
}
}
runHook().catch((error) => {
if (process.env.LANGFUSE_CODEX_DEBUG === "true") console.error("[langfuse-codex] fatal:", error);
if (failOnError) process.exitCode = 1;
});

//#endregion
Expand Down
2 changes: 1 addition & 1 deletion plugins/tracing/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"hooks": [
{
"type": "command",
"command": "node ./plugins/tracing/dist/index.mjs",
"command": "node \"${CODEX_HOME:-$HOME/.codex}/plugins/cache/codex-observability-plugin/tracing/0.1.0/dist/index.mjs\"",
"timeout": 30,
"statusMessage": "Uploading Codex trace to Langfuse"
}
Expand Down
7 changes: 6 additions & 1 deletion plugins/tracing/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,20 @@ export const ConfigSchema = z.object({
max_chars: z.number().int().positive(),
// LANGFUSE_CODEX_DEBUG
debug: z.boolean(),
// LANGFUSE_CODEX_FAIL_ON_ERROR
fail_on_error: z.boolean(),
});

export type Config = z.infer<typeof ConfigSchema>;

const PartialConfigSchema = ConfigSchema.partial();

const DEFAULTS: Pick<Config, "enabled" | "base_url" | "max_chars" | "debug"> = {
const DEFAULTS: Pick<Config, "enabled" | "base_url" | "max_chars" | "debug" | "fail_on_error"> = {
enabled: false,
base_url: "https://cloud.langfuse.com",
max_chars: 20_000,
debug: false,
fail_on_error: false,
};

function parseBoolean(value: unknown): boolean | undefined {
Expand Down Expand Up @@ -118,6 +121,7 @@ async function readConfigFile(file: string): Promise<Partial<Config> | undefined
metadata: raw.metadata != null ? parseMetadata(raw.metadata) : undefined,
max_chars: raw.max_chars != null ? parseInteger(raw.max_chars) : undefined,
debug: raw.debug != null ? parseBoolean(raw.debug) : undefined,
fail_on_error: raw.fail_on_error != null ? parseBoolean(raw.fail_on_error) : undefined,
}),
);
} catch {
Expand All @@ -142,6 +146,7 @@ function readEnvConfig(env: Record<string, string | undefined>): Partial<Config>
metadata: parseMetadata(env.LANGFUSE_CODEX_METADATA),
max_chars: parseInteger(env.LANGFUSE_CODEX_MAX_CHARS),
debug: parseBoolean(env.LANGFUSE_CODEX_DEBUG),
fail_on_error: parseBoolean(env.LANGFUSE_CODEX_FAIL_ON_ERROR),
}),
);
}
Expand Down
14 changes: 12 additions & 2 deletions plugins/tracing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { convertRollout } from "./trace.js";
import type { HookInput } from "./types.js";
import { debugLog, readStdin, setDebug } from "./utils.js";

let failOnError = process.env.LANGFUSE_CODEX_FAIL_ON_ERROR === "true";

/**
* Entry point for the Codex `Stop` hook.
*
Expand All @@ -12,7 +14,9 @@ import { debugLog, readStdin, setDebug } from "./utils.js";
* transcript into Langfuse traces.
*
* The hook fails open: any error is logged (in debug mode) and swallowed so a
* tracing problem never blocks the Codex session.
* tracing problem never blocks the Codex session. Set
* `LANGFUSE_CODEX_FAIL_ON_ERROR=true` while testing if you want Codex to report
* hook failures instead.
*/
export async function runHook(): Promise<void> {
let hookInput: HookInput;
Expand All @@ -25,6 +29,7 @@ export async function runHook(): Promise<void> {

const config = await getConfig();
setDebug(config.debug);
failOnError = config.fail_on_error;

if (!config.enabled) {
debugLog("tracing disabled (set TRACE_TO_LANGFUSE=true to enable)");
Expand All @@ -44,19 +49,24 @@ export async function runHook(): Promise<void> {
await convertRollout(hookInput.transcript_path, { config });
} catch (error) {
debugLog("failed to convert rollout:", error);
if (config.fail_on_error) throw error;
} finally {
try {
await instrumentation.shutdown();
} catch (error) {
debugLog("error during flush/shutdown:", error);
if (config.fail_on_error) throw error;
}
}
}

runHook().catch((error) => {
// Last-resort guard: never throw out of the hook.
// Last-resort guard: fail open unless explicitly requested for testing.
if (process.env.LANGFUSE_CODEX_DEBUG === "true") {
// eslint-disable-next-line no-console
console.error("[langfuse-codex] fatal:", error);
}
if (failOnError) {
process.exitCode = 1;
}
});
4 changes: 4 additions & 0 deletions plugins/tracing/src/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ export async function convertRollout(
if (turn.completed && turn.turnId) {
uploaded.add(turn.turnId);
await markTurnUploaded(rolloutFile, turn.turnId);
} else if (turn.turnId) {
debugLog(
`uploaded in-progress turn ${turn.turnId}; waiting for completion before sidecar mark`,
);
}
}
}
18 changes: 18 additions & 0 deletions plugins/tracing/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe("getConfig", () => {
expect(config.enabled).toBe(false);
expect(config.base_url).toBe("https://cloud.langfuse.com");
expect(config.max_chars).toBe(20_000);
expect(config.fail_on_error).toBe(false);
});

it("reads credentials and enable flag from environment variables", async () => {
Expand Down Expand Up @@ -118,4 +119,21 @@ describe("getConfig", () => {
const config = await getConfig({ home, cwd: emptyHome(), env: {} });
expect(config.enabled).toBe(false);
});

it("parses fail-on-error from config and environment", async () => {
const home = makeTmpHome({
rel: ".codex/langfuse.json",
contents: { fail_on_error: "true" },
});

const fromFile = await getConfig({ home, cwd: emptyHome(), env: {} });
expect(fromFile.fail_on_error).toBe(true);

const fromEnv = await getConfig({
home,
cwd: emptyHome(),
env: { LANGFUSE_CODEX_FAIL_ON_ERROR: "false" },
});
expect(fromEnv.fail_on_error).toBe(false);
});
});
Loading
Loading