Skip to content

feat(tracing): attach Codex turns to external parent spans - #72

Open
zenor0 wants to merge 2 commits into
langfuse:mainfrom
zenor0:main
Open

feat(tracing): attach Codex turns to external parent spans#72
zenor0 wants to merge 2 commits into
langfuse:mainfrom
zenor0:main

Conversation

@zenor0

@zenor0 zenor0 commented Sep 6, 2026

Copy link
Copy Markdown

Motivation

When an application launches Codex as one step in an existing agent run, the plugin currently exports each Codex turn as a separate Langfuse trace. The application-level work and the plugin's detailed Codex observations therefore appear separately, even though they belong to the same operation.

This PR adds an opt-in attached mode: the launcher passes a W3C traceparent, and the plugin attaches its existing observation tree beneath that application span. It retains the rollout-derived prompts, outputs, reasoning, model usage, tool results, errors, and subagent observations.

trace_seed remains useful for predictable IDs, but its synthetic parent does not establish a parent-child relationship with a real application span. This change supplies that relationship explicitly.

Usage And Result

With tracing already enabled, a launcher can pass the context of a sampled parent span when starting its dedicated Codex process:

# TRACE_ID and PARENT_SPAN_ID come from the launcher's actual parent span.
LANGFUSE_CODEX_TRACEPARENT="00-${TRACE_ID}-${PARENT_SPAN_ID}-01" \
  codex exec "your prompt"

The resulting observations share the application's trace ID:

Application run
|-- Application-side work
|-- Codex Turn
|   |-- LLM
|   |   `-- exec_command
|   `-- Codex Subagent Turn
`-- Codex Turn
    `-- LLM

Every top-level Codex turn uses the supplied application span as its parent. Generations, tools, and subagents retain their existing nesting. The launcher and plugin must export to the same Langfuse project to see the combined trace.

Design Decisions

  1. Use W3C traceparent for the external context. It carries the trace ID, parent span ID, and sampling flags together. Parsing reuses OpenTelemetry's parseTraceParent, and the resulting span context is marked remote before being passed through the existing conversion path.

  2. Bind context to a dedicated process. The intended lifecycle is one application run owning one Codex CLI or App Server process. A process-scoped environment variable fits that lifecycle and keeps the implementation within this plugin. A long-lived App Server shared by unrelated runs would need request-level context propagation into the Stop hook; this PR does not implement that routing. Previously processed turns are still governed by the existing sidecar and are not moved to another trace when the parent changes.

  3. Keep parent context transient and explicitly scoped. LANGFUSE_CODEX_TRACEPARENT is read only from the hook process environment. It is not a langfuse.json setting and should not be persisted in a shell profile, where unrelated runs could inherit it. The plugin does not consume unscoped TRACEPARENT or propagate tracestate.

  4. Leave trace ownership with the launching application. In attached mode, the plugin does not propagate its configured trace name, session, user, tags, or metadata. Codex-specific observation metadata is still emitted. The conversion also carries the application's Langfuse trace-ID claim in the active context, preventing the real LangfuseSpanProcessor from marking the cross-process Codex span as another application root.

  5. Honor the parent's sampling decision. Attached mode uses a parent-based sampler so the upstream sampled bit is authoritative, including when OTEL_TRACES_SAMPLER is set. Standalone mode leaves sampler selection to the existing OpenTelemetry configuration.

The ownership integration uses setLangfuseTraceIdInBaggage from @langfuse/core, which is already a transitive dependency and is now declared directly. This helper is exported but marked @internal by the SDK; that dependency is a specific point for maintainer review. Tests use the real Langfuse processor with an in-memory exporter to verify that attached observations are not application roots, while standalone observations remain roots.

Behavior

Input Result
No external parent Existing standalone behavior, including trace_seed and sampler environment configuration.
Valid external parent and trace_seed The explicit parent takes precedence; seed derivation is skipped.
Sampled external parent Codex observations are recorded beneath the supplied parent.
Unsampled external parent No Codex spans are exported; completed turns are still marked processed in the dedup sidecar.
Invalid external parent Debug logging and the existing seed/auto-generated trace behavior. With LANGFUSE_CODEX_FAIL_ON_ERROR=true, reject the invalid value instead.

The invalid-input behavior follows the hook's existing fail-open policy so tracing configuration errors do not block a Codex run by default.

Commit Structure

The first commit contains the feature and its local regression tests and is independently usable. The second commit adds optional backend E2E coverage and the test:e2e script; it can be dropped without affecting the feature or regular test suite.

Validation

The following local checks passed both on the standalone feature commit and with the optional E2E commit applied:

  • pnpm test: 51 tests passed. Coverage includes parentage across multiple turns and subagents, seed precedence, parsing and failure behavior, sampling, and SDK application-root ownership.
  • pnpm run lint: formatting, TypeScript checks, and build passed.
  • pnpm exec npm pack ./plugins/tracing --dry-run --json: the package includes the generated hook. Following the current upstream contribution workflow, no dist artifacts are committed.

Ownership checks share the existing trace fixtures and real processor setup rather than a separate test harness. The opt-in E2E uses the installed Langfuse API client and Node/Vitest utilities for process execution, polling, and cleanup.

The PR also adds an opt-in pnpm run test:e2e test. It creates a real application span, runs the bundled hook against a fixture, checks the persisted parent-child tree through the Langfuse observations API, and deletes the temporary trace. It requires Langfuse credentials and was not run for this final commit; the regular test suite makes no requests to Langfuse.

Opening as a draft for feedback on the process-scoped propagation contract and the SDK trace-ownership integration.

@CLAassistant

CLAassistant commented Sep 6, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Accept LANGFUSE_CODEX_TRACEPARENT to attach the complete Codex observation tree to a process-scoped W3C parent span. Honor upstream sampling and preserve application trace ownership.

Preserve standalone sampling and deterministic trace IDs. Reuse the existing fixtures with a real Langfuse processor for local regression coverage. Keep npm scripts unchanged.

Validation: 51 local tests, full lint, and an npm pack dry run pass. Generated bundles remain untracked.
Add an opt-in credentialed test for a real application span and bundled Codex hook, with a dedicated Vitest config, test:e2e script, and usage documentation.

This commit can be dropped without affecting the tracing feature or regular test suite.

Validation: 51 local tests, full lint, and an npm pack dry run pass. The real-backend E2E test was not run.
@zenor0
zenor0 marked this pull request as ready for review September 6, 2026 06:18

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants