Rewrite plugin from Python to TypeScript (running on Bun)#1
Open
jannikmaierhoefer wants to merge 9 commits into
Open
Rewrite plugin from Python to TypeScript (running on Bun)#1jannikmaierhoefer wants to merge 9 commits into
jannikmaierhoefer wants to merge 9 commits into
Conversation
Replace the Python Stop hook with a self-contained, pre-bundled TypeScript hook so the plugin runs on Node.js (>=20) instead of requiring Python and the langfuse Python SDK — which was painful to set up on macOS. - src/: hook entry, config resolution, transcript parsing, Langfuse emit, incremental sidecar state, OTel instrumentation - dist/index.mjs: committed self-contained bundle (tsdown), run directly by the Stop hook with no install step - Spans sent via the Langfuse TypeScript SDK over OpenTelemetry, batched - Incremental byte-offset reader + per-transcript dedup sidecar replaces the global state file and file lock - vitest tests for parsing and config resolution - Updated hooks.json to invoke node, refreshed README, bumped to 2.0.0 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generation observations are now all named "Claude Generation" instead of "Claude Generation 1", "Claude Generation 2", … The per-step index is still available on the observation via the claude.step_index metadata field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships the "Claude Generation" observation rename. Patch bump so installs tracking the branch pick up the new bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… its name The root turn observation is now an agent-type observation named "Claude Code Turn" (was a span named "Claude Code - Turn N"); the trace name matches. The turn number remains available via the claude.turn_number metadata field. Release 2.0.2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When no user_id is configured (via CC_LANGFUSE_USER_ID, env, or langfuse.json), fall back to the email of the logged-in Claude Code user, read from ~/.claude.json (oauthAccount.emailAddress). This attaches the user to every trace/observation so sessions can be filtered by user in Langfuse. An explicit user_id still takes precedence. Release 2.1.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude Code records each subagent (spawned via the Agent/Task tool) as its own transcript at <project>/<sessionId>/subagents/agent-<id>.jsonl, with a sibling .meta.json whose toolUseId links it to the spawning tool_use block in the main transcript. Discover those subagent transcripts, parse them with the same turn builder, and nest each subagent's generations and tool calls under the exact tool call that launched it. Nesting recurses (a subagent can spawn subagents) and is guarded against cycles via a visited set. - src/subagents.ts: discover subagents and index them by spawning tool_use id - src/state.ts: factor out parseRows / readAllRows for full-file reads - src/trace.ts: async emit pipeline with a reusable emitSteps helper; expand subagents under their tool observation - tests for discovery; README documents the new Subagents behavior Release 2.2.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- hooks.json now runs the committed bundle with bun instead of node - Bundle dist/index.mjs with 'bun build' (single self-contained ESM file, dynamic imports inlined) instead of tsdown; drop tsdown.config.ts - Port tests from vitest to Bun's built-in test runner (bun:test) - Replace pnpm with bun: bun.lock instead of pnpm-lock.yaml, scripts use bun, engines require bun >= 1.2, packageManager dropped - Update README prerequisites and development docs to Bun - Bump plugin version to 3.0.0 (runtime prerequisite change) Verified end-to-end under Bun against a mock Langfuse OTLP endpoint: turn/generation/tool span tree, session-id and user-id context propagation via AsyncLocalStorage, token usage, backdated timestamps, exactly-once sidecar dedup, and fail-open behavior all intact. https://claude.ai/code/session_015es18wH2qWjsuPtZrXQCjA
Keeps the TS/Bun hook as the implementation: hooks run the bundled dist/index.mjs (now on SessionEnd as well as Stop, matching main), the Python hook stays deleted, and main's plugin rename to 'langfuse-observability' plus the new userConfig options are taken. The new Python-hook features merged on main (LANGFUSE_USER_ID, isMeta row handling, skill tags, skill content capture, cwd/git-branch metadata) are ported to the TypeScript hook in the follow-up commit. https://claude.ai/code/session_015es18wH2qWjsuPtZrXQCjA
Brings the TS rewrite up to parity with the features merged to main after the rewrite branched: - LANGFUSE_USER_ID env/plugin option (CC_LANGFUSE_USER_ID still works) - Skip isMeta transcript rows (slash-command expansions, skill instructions) so they no longer create phantom turns - Tag traces skill:<name> per invoked skill (CC_LANGFUSE_SKILL_TAGS, default on) - Optionally attach injected skill instructions to the Skill tool span (CC_LANGFUSE_CAPTURE_SKILL_CONTENT, default off) - Surface the turn's cwd and git branch as trace metadata - Run the hook on SessionEnd as well as Stop (done in the merge commit) Adds bun tests for all of the above (24 total) and documents the new options in the README. Verified end-to-end against a mock Langfuse OTLP endpoint: skill tag propagated to every span, injected instructions only on the Skill tool span, cwd/git_branch on the turn, and the isMeta row no longer splits the turn. https://claude.ai/code/session_015es18wH2qWjsuPtZrXQCjA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Rewrites the Claude Code observability plugin from Python to a self-contained, pre-bundled TypeScript hook. The plugin now runs on Bun (>= 1.2) instead of requiring
python3and thelangfusePython SDK.The structure and conventions mirror the sibling codex-observability-plugin: one self-contained
dist/index.mjsinvoked directly by the hook, deps bundled at build time, Langfuse TS SDK over OpenTelemetry.Why
The Python version was painful to get running on macOS — it depended on a system
python3havinglangfuse>=4.0,<5importable, and silently no-op'd when the SDK wasn't available. Shipping a committed bundle means zero install/build step for users; Bun runs it directly.What changed
src/— hook entry (index.ts), config resolution (config.ts), transcript parsing (parse.ts), Langfuse emit (trace.ts), incremental sidecar state (state.ts), OTel setup (instrumentation.ts), helpers (utils.ts,types.ts).dist/index.mjs— committed self-contained bundle (bun build, single ESM file with dynamic imports inlined). All runtime deps (Langfuse SDK, OpenTelemetry, zod) bundled; only Node built-ins (which Bun provides) external. Run directly by the hooks, no install step.hooks/hooks.json— invokesbun "${CLAUDE_PLUGIN_ROOT}/dist/index.mjs"onStopandSessionEnd(waspython3 .../langfuse_hook.py), with a 30s timeout.~/.claude/state/langfuse_state.json+ file lock with an incremental byte-offset reader and a per-transcript sidecar (.jsonl.langfuse) storing{offset, turnCount}, so each turn uploads exactly once.hooks/langfuse_hook.py.mainand ported its Python-hook features to TS (no merged functionality is dropped): thelangfuse-observabilityplugin rename,LANGFUSE_USER_IDoption,isMetarow handling (slash-command/skill injections no longer create phantom turns),skill:<name>trace tags (CC_LANGFUSE_SKILL_TAGS, default on), optional capture of injected skill instructions on the Skill tool span (CC_LANGFUSE_CAPTURE_SKILL_CONTENT, default off), and cwd/git-branch trace metadata.bun install(bun.lock),bun buildfor bundling, nativebun testfor the test suite,tsc --noEmit+ prettier for linting..gitignorekeepsdist/, ignores*.jsonl.langfuse.Trace structure (unchanged semantics)
One trace per turn, grouped into a Langfuse session by session id; one
generationper assistant message with model + token usage (including cache read/write); nestedtoolobservations matchedtool_use→tool_result; subagent transcripts expanded inline under the spawning tool call. Original timestamps preserved on every span. Fails open — a tracing error never blocks the session.Config
Resolved as defaults →
~/.claude/langfuse.json→<cwd>/.claude/langfuse.json→ env. For each setting, the Claude Code plugin-config form (CLAUDE_PLUGIN_OPTION_<NAME>) wins, then plainLANGFUSE_*, thenCC_LANGFUSE_*aliases — preserving the keys the existinguserConfiginplugin.jsonalready exposes. Optional knobs:environment,user_id,tags,metadata,max_chars,skill_tags,capture_skill_content,fail_on_error.Testing
bun run lint(prettier +tsc --noEmit+ dist-is-current check) andbun test(24 tests) pass.session.id/user.id/tags propagated to every span (AsyncLocalStorage context propagation works on Bun), correct usage and backdated timestamps, exactly-once sidecar dedup, and fail-open confirmed under a real network failure (exit 0 by default, exit 1 withCC_LANGFUSE_FAIL_ON_ERROR=true).skill:<name>tag on every span of the trace, injected skill instructions only on the Skill tool span, cwd/git-branch metadata on the turn, andisMetarows no longer splitting turns.bun buildoutput confirmed byte-for-byte deterministic (keeps thelint:distCI guard meaningful).Reviewer notes
dist/index.mjsis intentionally committed (large diff) — that's the artifact the hook runs. CI guard (lint:dist) rebuilds and fails if it drifts fromsrc/. Rebuild + commit after anysrc/change.mainor a tag).--target node, so it also runs under Node.js >= 20 unchanged — only the hook command and tooling assume Bun.Stop/SessionEnd-hook tracing, matching the originallangfuse_hook.py.🤖 Generated with Claude Code