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
15 changes: 8 additions & 7 deletions TELEMETRY-CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ lost.
| root | `invoke_agent` | Literal. No interpolation. |
| model turn | `chat {model}` | For example `chat claude-3-5-sonnet-20241022`. |
| tool call | `execute_tool {tool_name}` | For example `execute_tool get_weather`. |
| graph | `ld.ai.graph` | Already correct in Python. Do not touch. See section 8. |
| graph | `launchdarkly.graph` | LaunchDarkly-owned, so it takes the `launchdarkly.` prefix. See section 10. |

The model goes in the `chat` span name because the semantic conventions name an inference span
`{gen_ai.operation.name} {gen_ai.request.model}`. A bare `chat` aggregates more neatly but tells a
Expand Down Expand Up @@ -130,7 +130,7 @@ An app that opens a fresh CLI session per turn and re-feeds history must pass it
turn becomes its own conversation. See section 4 for the third place it appears.

When a conversation id is bound, every handler stamps it on root, `chat`, and `execute_tool` (and
on `ld.ai.graph`) via the shared span processor. No id is invented when the caller supplies none.
on `launchdarkly.graph`) via the shared span processor. No id is invented when the caller supplies none.

---

Expand Down Expand Up @@ -641,12 +641,13 @@ implement this as a passthrough.

## 10. What not to touch

The graph span is already at parity. `ld.ai.graph` with `ld.ai.graph.key` and `ld.ai.graph.path`
matches between `client/graph.ts` and `graph.py`, and across all three `native_graph` pairs.
The graph span is at parity. `launchdarkly.graph` with `launchdarkly.graph.key` and
`launchdarkly.graph.path` matches across all three `native_graph` pairs.

`js-ai-sdk` PR #16 renames these three to `launchdarkly.graph`, `launchdarkly.graph.key` and
`launchdarkly.graph.path`. If that PR merges, make the same rename here, in `graph.py` and the
three `native_graph.py`. If it does not, change nothing.
These used to be `ld.ai.graph`, `ld.ai.graph.key` and `ld.ai.graph.path`. `js-ai-sdk` #16 renamed
them so the graph span and the `invoke_agent` root share one name for the graph key, and so
everything LaunchDarkly owns on a span sits under `launchdarkly.`. `ld.ai.` stays reserved for
metric and event keys.

Nothing else in `graph.py` or `native_graph.py` needs work.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ async def invoke(

tracer_name = "@launchdarkly/ai-claude-agents"
if _HAS_OTEL:
span = trace.get_tracer(tracer_name).start_span("ld.ai.graph")
span.set_attribute("ld.ai.graph.key", def_obj.key)
span = trace.get_tracer(tracer_name).start_span("launchdarkly.graph")
span.set_attribute("launchdarkly.graph.key", def_obj.key)
else:
span = None

Expand Down Expand Up @@ -383,7 +383,7 @@ async def _subagent_execute(
graph_dur = int((time.monotonic() - start_time) * 1000)

if span:
span.set_attribute("ld.ai.graph.path", "->".join(path))
span.set_attribute("launchdarkly.graph.path", "->".join(path))
span.set_attribute("gen_ai.usage.input_tokens", total_usage["input"])
span.set_attribute("gen_ai.usage.output_tokens", total_usage["output"])
span.set_attribute("gen_ai.usage.total_tokens", total_usage["total"])
Expand Down
4 changes: 2 additions & 2 deletions packages/claude-agents/tests/test_native_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ async def test_build_tool_mcp_throws_when_tool_not_in_handlers(self) -> None:


class TestNativeGraphConversationId:
"""The telemetry contract claims the conversation id reaches ``ld.ai.graph`` spans.
"""The telemetry contract claims the conversation id reaches ``launchdarkly.graph`` spans.

The span is opened after an ``await`` on the graph definition, so this pins that the binding
survives the await chain rather than only covering spans started synchronously in the block.
Expand Down Expand Up @@ -757,7 +757,7 @@ async def test_stamps_conversation_id_on_graph_span(self) -> None:
)

graph_spans = [
s for s in exporter.get_finished_spans() if s.name == "ld.ai.graph"
s for s in exporter.get_finished_spans() if s.name == "launchdarkly.graph"
]
assert len(graph_spans) == 1
assert (graph_spans[0].attributes or {}).get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ async def invoke(

tracer_name = "@launchdarkly/ai-langchain-agents"
if _HAS_OTEL:
span = trace.get_tracer(tracer_name).start_span("ld.ai.graph")
span.set_attribute("ld.ai.graph.key", def_obj.key)
span = trace.get_tracer(tracer_name).start_span("launchdarkly.graph")
span.set_attribute("launchdarkly.graph.key", def_obj.key)
else:
span = None

Expand Down Expand Up @@ -395,7 +395,7 @@ def _content_str(msg: Any) -> str:
final_output = _content_str(last_msg)

if span:
span.set_attribute("ld.ai.graph.path", "->".join(path))
span.set_attribute("launchdarkly.graph.path", "->".join(path))
span.set_attribute("gen_ai.usage.input_tokens", total_usage["input"])
span.set_attribute("gen_ai.usage.output_tokens", total_usage["output"])
span.set_attribute("gen_ai.usage.total_tokens", total_usage["total"])
Expand Down
6 changes: 4 additions & 2 deletions packages/langchain-agents/tests/test_native_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,9 +574,11 @@ async def test_otel_span_has_graph_key_attribute(self) -> None:
with patch.object(ng_mod, "_HAS_OTEL", True):
await to_lang_graph(_make_def_promise(graph_def)).invoke("hi")

mock_trace.get_tracer.return_value.start_span.assert_called_with("ld.ai.graph")
mock_trace.get_tracer.return_value.start_span.assert_called_with(
"launchdarkly.graph"
)
calls = {c[0][0]: c[0][1] for c in mock_span.set_attribute.call_args_list}
assert "ld.ai.graph.key" in calls
assert "launchdarkly.graph.key" in calls

@pytest.mark.asyncio
async def test_terminal_leaf_connected_to_end(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ async def invoke(

tracer_name = "@launchdarkly/ai-openai-agents"
if _HAS_OTEL:
span = trace.get_tracer(tracer_name).start_span("ld.ai.graph")
span.set_attribute("ld.ai.graph.key", def_obj.key)
span = trace.get_tracer(tracer_name).start_span("launchdarkly.graph")
span.set_attribute("launchdarkly.graph.key", def_obj.key)
else:
span = None

Expand Down Expand Up @@ -286,7 +286,7 @@ async def on_agent_start(self, context: Any, agent: Any) -> None:
duration = int((time.monotonic() - start_time) * 1000)

if span:
span.set_attribute("ld.ai.graph.path", "->".join(path))
span.set_attribute("launchdarkly.graph.path", "->".join(path))
span.set_attribute("gen_ai.usage.input_tokens", input_tokens)
span.set_attribute("gen_ai.usage.output_tokens", output_tokens)
span.set_attribute("gen_ai.usage.total_tokens", total_tokens)
Expand Down
12 changes: 6 additions & 6 deletions packages/openai-agents/tests/test_native_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ async def test_total_tokens_tracked_on_success(self) -> None:

@pytest.mark.asyncio
async def test_otel_span_has_graph_key_attribute(self) -> None:
"""OTel span must have ld.ai.graph.key attribute set to the graph key."""
"""OTel span must have launchdarkly.graph.key attribute set to the graph key."""
mock_span = MagicMock()
mock_trace = MagicMock()
mock_trace.get_tracer.return_value.start_span.return_value = mock_span
Expand All @@ -569,8 +569,8 @@ async def test_otel_span_has_graph_key_attribute(self) -> None:
set_attr_calls = {
c[0][0]: c[0][1] for c in mock_span.set_attribute.call_args_list
}
assert "ld.ai.graph.key" in set_attr_calls
assert set_attr_calls["ld.ai.graph.key"] == "test-graph"
assert "launchdarkly.graph.key" in set_attr_calls
assert set_attr_calls["launchdarkly.graph.key"] == "test-graph"

@pytest.mark.asyncio
async def test_agent_end_hook_emits_generation_success(self) -> None:
Expand Down Expand Up @@ -667,14 +667,14 @@ async def _run_and_fire_hooks(agent: Any, text: str, hooks: Any = None) -> Any:
with patch.object(_openai_ng, "_HAS_OTEL", True):
await to_openai_agents(_make_def_promise(graph_def)).invoke("hi")

# Extract the path from the span set_attribute call for "ld.ai.graph.path"
# Extract the path from the span set_attribute call for "launchdarkly.graph.path"
path_val: str | None = None
for call in mock_span.set_attribute.call_args_list:
if call[0][0] == "ld.ai.graph.path":
if call[0][0] == "launchdarkly.graph.path":
path_val = call[0][1]
break

assert path_val is not None, "ld.ai.graph.path attribute was not set"
assert path_val is not None, "launchdarkly.graph.path attribute was not set"
path_parts = [p for p in path_val.split("->") if p]
child_occurrences = path_parts.count("child")
assert child_occurrences <= 1, (
Expand Down
9 changes: 4 additions & 5 deletions tests/test_cross_handler_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,10 +380,9 @@ def test_the_langchain_provider_name_is_the_configured_name(
"feature_flag.context.id",
"feature_flag.contextKeys",
"context.contextKeys",
# Graph spans, unchanged from before the span work
"ld.ai.graph",
"ld.ai.graph.key",
"ld.ai.graph.path",
# Graph spans. The graph key is listed with the root attributes above.
"launchdarkly.graph",
"launchdarkly.graph.path",
}

#: Functions kept exported for one release that nothing calls any more.
Expand Down Expand Up @@ -415,7 +414,7 @@ def _without_superseded(source: str) -> str:
_KEY_PATTERN = re.compile(
r'set_attribute\(\s*f?"([^"{]+)"'
r'|add_event\(\s*"([^"]+)"'
r'|start_span\(\s*"(ld\.ai\.graph)"'
r'|start_span\(\s*"(launchdarkly\.graph)"'
r'|"(gen_ai\.[a-z_.0-9]+)"'
r'|f"(gen_ai\.[a-z_.]+)\.\{'
# The feature_flag event's own attributes are built as a plain dict before being handed to
Expand Down
Loading