Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@
"type": "string",
"title": "Trace ID seed",
"description": "Optional. Derive deterministic trace ids as Langfuse.create_trace_id(seed=f\"{seed}:{turn_number}\") so external systems can precompute them. Use a unique seed per session (e.g. the --session-id UUID). Leave empty for auto-generated ids."
},
"CC_LANGFUSE_TAGS": {
"type": "string",
"title": "Custom tags",
"description": "Optional. Comma-separated custom tags appended to every trace (e.g. env:prod,team:platform)."
}
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ The plugin requires or accepts:
| `CC_LANGFUSE_SKILL_TAGS` | Tag traces with `skill:<name>` for every skill invoked in the turn (default true). |
| `CC_LANGFUSE_CAPTURE_SKILL_CONTENT` | Include injected skill instruction text in the Skill tool span output (default false). |
| `CC_LANGFUSE_TRACE_SEED` | Optional. Opt-in seed for deterministic trace IDs — see [Deterministic trace IDs](#deterministic-trace-ids). |
| `CC_LANGFUSE_TAGS` | Optional. Comma-separated custom tags appended to every trace (e.g. `env:prod,team:platform`). |

Get keys from your Langfuse project settings → API Keys.

Expand Down
3 changes: 2 additions & 1 deletion hooks/langfuse_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def _opt(name: str) -> str:

DEBUG = _opt("CC_LANGFUSE_DEBUG").lower() == "true"
SKILL_TAGS = (_opt("CC_LANGFUSE_SKILL_TAGS") or "true").lower() == "true"
CUSTOM_TAGS = [t.strip() for t in _opt("CC_LANGFUSE_TAGS").split(",") if t.strip()]
CAPTURE_SKILL_CONTENT = _opt("CC_LANGFUSE_CAPTURE_SKILL_CONTENT").lower() == "true"
try:
MAX_CHARS = int(_opt("CC_LANGFUSE_MAX_CHARS") or "20000")
Expand Down Expand Up @@ -1216,7 +1217,7 @@ def get_trace_tags(
turn: Turn,
subagent_transcripts_by_tool_use_id: Optional[Dict[str, Dict[str, Any]]] = None,
) -> List[str]:
tags = ["claude-code"]
tags = ["claude-code"] + CUSTOM_TAGS
if SKILL_TAGS:
tags += collect_skill_tags(turn)
tags += collect_subagent_skill_tags(turn, subagent_transcripts_by_tool_use_id)
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/test_skill_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ def make_attributed_assistant_row(skill: str, uuid: str = "assistant-attr") -> d
}


def make_plain_assistant_row(uuid: str = "assistant-plain") -> dict[str, Any]:
"""A reply with no skill involved, just to close out a turn."""
return {
"type": "assistant",
"timestamp": "2026-01-01T00:00:01.000Z",
"uuid": uuid,
"message": {
"id": f"msg-{uuid}",
"role": "assistant",
"model": "claude-test",
"content": [{"type": "text", "text": "Hi there."}],
},
}


def collect_tags(hook_module, rows: list[dict[str, Any]]) -> list[str]:
turns = hook_module.build_turns(rows)
assert len(turns) == 1
Expand Down Expand Up @@ -172,3 +187,35 @@ def test_same_skill_across_two_subagents_is_tagged_once(hook_module, tmp_path):
assert hook_module.collect_subagent_skill_tags(turns[0], sub_map) == [
"subagent-skill:deep-research",
]


def test_custom_tags_are_appended_to_every_trace(hook_module, monkeypatch):
monkeypatch.setattr(hook_module, "CUSTOM_TAGS", ["env:prod", "team:platform"])
rows = [make_user_row("Hello."), make_plain_assistant_row()]
turns = hook_module.build_turns(rows)

assert hook_module.get_trace_tags(turns[0]) == [
"claude-code",
"env:prod",
"team:platform",
]


def test_no_custom_tags_leaves_trace_tags_unchanged(hook_module, monkeypatch):
monkeypatch.setattr(hook_module, "CUSTOM_TAGS", [])
rows = [make_user_row("Hello."), make_plain_assistant_row()]
turns = hook_module.build_turns(rows)

assert hook_module.get_trace_tags(turns[0]) == ["claude-code"]


def test_custom_tags_combine_with_skill_tags(hook_module, monkeypatch):
monkeypatch.setattr(hook_module, "CUSTOM_TAGS", ["env:prod"])
rows = [make_user_row("Do a thing."), make_skill_tool_use_row("claude-api")]
turns = hook_module.build_turns(rows)

assert hook_module.get_trace_tags(turns[0]) == [
"claude-code",
"env:prod",
"skill:claude-api",
]