feat(hook): custom trace tags and session grouping via commands - #27
zerafachris wants to merge 1 commit into
Conversation
Adds two opt-in, related config options for attributing traces to external work items, both empty by default (no behavior change for existing users): - CC_LANGFUSE_TAG_COMMAND: run once per hook run; each non-empty stdout line is merged into the trace tags (de-duped, order-preserving). - CC_LANGFUSE_SESSION_LABEL_COMMAND + CC_LANGFUSE_SESSION_LABEL_MODE: use the first stdout line as a label to group the session's traces (prefix / collapse / off). One script can drive both -- tags use every line, the label uses the first. Session labeling changes only the trace grouping id; the incremental-read state key stays on the raw session id, so relabeling never re-emits past turns and needs no state migration. Both paths are fail-open and bounded (timeout, tag count/length, 199-char session id cap). Adds tests/unit/test_custom_tags.py (15 cases). Full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
|
Opt-in and backward-compatible — all four config options default to empty/existing behavior, so nothing changes for current users. Follows the same Ran the full test suite locally against this repo's @hassiebp @jannikmaierhoefer — would appreciate a look when you have a moment. Happy to adjust the shape (e.g. no-shell |
|
@zerafachris you noted in the description that a static They compose rather than compete: the static form covers the case where the launching process One practical heads-up: this PR adds |
feat(hook): custom trace tags via CC_LANGFUSE_TAG_COMMAND
What
Adds two opt-in, related config options for attributing traces to external work items.
Both empty by default — no behavior change for existing users.
CC_LANGFUSE_TAG_COMMAND— runs a command once per hook run; each non-empty stdoutline is merged into the trace tags.
CC_LANGFUSE_SESSION_LABEL_COMMAND+CC_LANGFUSE_SESSION_LABEL_MODE— takes thefirst non-empty stdout line as a label and groups the session's traces under it
(
prefix/collapse/off).One script can drive both — tags use every line, the label uses the first:
That tags every trace with
sc-<id>+layer:buildand groups the session assc-<id>/<uuid>.Why
Today
get_trace_tags()returns a fixed set (claude-code+ optional skill tags), so anyteam wanting to attribute traces to a branch, ticket, CI run, or cost center must fork and
patch the hook. Because the hook is large and evolves fast, such patches silently stop
applying on update — and since the hook fails open, the custom tags just vanish with no
error. This gives those teams a supported, update-safe seam instead.
How
collect_custom_tags()runs the command (shell=True), memoized per process;one line in
get_trace_tags()appends the result and de-dups order-preserving (acommand may re-emit
claude-code).apply_session_label()(called inmain()) relabels the session id usedfor the trace. Crucially, the incremental-read state key stays on the raw session id —
emit_new_turns_from_transcriptgains atrace_session_idparam, so relabeling groupstraces without ever re-emitting past turns or requiring a state migration.
_run_tag_command) and are fail-open in every direction — nocommand, non-zero exit, timeout, or exception contributes nothing and never interrupts
tracing.
CC_LANGFUSE_TAG_TIMEOUT(default 2s), max 20 tags, 64 chars per tag/label,session id capped at Langfuse's 199-char limit.
Surface
hooks/langfuse_hook.py:+import subprocess, config constants,_run_tag_command(),collect_custom_tags(),resolve_session_label(),apply_session_label(), one line inget_trace_tags(), atrace_session_idparam threaded throughemit_new_turns_from_transcript,and the
apply_session_label()call inmain()..claude-plugin/plugin.json:userConfigentries for the four options.Testing
15 tests pass under pytest against the built hook. Tags:
['sc-1234','layer:build']['sc-9','foo'][][][]claude-code→ merge+de-dup['claude-code','sc-77']Session label:
sc-53855/<uuid>sc-53855Note for reviewers
CC_LANGFUSE_TAG_COMMANDexecutes a user-configured shell string (same trust level asconfiguring a hook; empty by default). If you'd rather avoid
shell=True, anargv-styleno-shell form is a one-line change — happy to switch. A static
CC_LANGFUSE_EXTRA_TAGSlist could also ship alongside for the no-subprocess case; left out here to keep the PR
focused on the dynamic use case that actually needs it.