mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-09 16:08:31 +00:00
fix(langfuse): resolve trace user from runtime context (#3794)
* fix(langfuse): resolve trace user from runtime context The worker built langfuse_user_id from get_effective_user_id(), which reads the request-scoped _current_user ContextVar. For runs invoked over an internal token on behalf of an end user, that ContextVar is never the end user, so traces recorded langfuse_user_id="default". Switch to resolve_runtime_user_id(runtime), matching the sandbox middleware/tools sites: it reads runtime.context["user_id"] (the owner carried in the run request's context, which survives background-task boundaries) and falls back to get_effective_user_id() for no-auth / browser paths. Caller-supplied metadata still wins via inject_langfuse_metadata's setdefault. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
4d660b202a
commit
84bbdf6e00
2 changed files with 66 additions and 10 deletions
|
|
@ -49,7 +49,7 @@ from deerflow.runtime.goal import (
|
||||||
)
|
)
|
||||||
from deerflow.runtime.serialization import serialize
|
from deerflow.runtime.serialization import serialize
|
||||||
from deerflow.runtime.stream_bridge import StreamBridge
|
from deerflow.runtime.stream_bridge import StreamBridge
|
||||||
from deerflow.runtime.user_context import get_effective_user_id
|
from deerflow.runtime.user_context import resolve_runtime_user_id
|
||||||
from deerflow.trace_context import DEERFLOW_TRACE_METADATA_KEY, get_current_trace_id, normalize_trace_id
|
from deerflow.trace_context import DEERFLOW_TRACE_METADATA_KEY, get_current_trace_id, normalize_trace_id
|
||||||
from deerflow.tracing import inject_langfuse_metadata
|
from deerflow.tracing import inject_langfuse_metadata
|
||||||
from deerflow.utils.messages import message_to_text
|
from deerflow.utils.messages import message_to_text
|
||||||
|
|
@ -338,7 +338,7 @@ async def run_agent(
|
||||||
inject_langfuse_metadata(
|
inject_langfuse_metadata(
|
||||||
config,
|
config,
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
user_id=get_effective_user_id(),
|
user_id=resolve_runtime_user_id(runtime),
|
||||||
assistant_id=record.assistant_id,
|
assistant_id=record.assistant_id,
|
||||||
model_name=record.model_name,
|
model_name=record.model_name,
|
||||||
environment=os.environ.get("DEER_FLOW_ENV") or os.environ.get("ENVIRONMENT"),
|
environment=os.environ.get("DEER_FLOW_ENV") or os.environ.get("ENVIRONMENT"),
|
||||||
|
|
|
||||||
|
|
@ -132,23 +132,79 @@ async def test_run_agent_injects_langfuse_metadata(monkeypatch):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_run_agent_falls_back_to_default_user_when_unset(monkeypatch):
|
async def test_run_agent_uses_context_user_id_over_contextvar(monkeypatch):
|
||||||
"""When no user is in the contextvar, langfuse_user_id falls back to 'default'.
|
"""A run carrying ``context.user_id`` traces to that user, not the contextvar.
|
||||||
|
|
||||||
Uses ``monkeypatch.setattr`` to redirect ``get_effective_user_id`` to return
|
Internal-token callers invoke a run on behalf of an end user, so the
|
||||||
``"default"`` rather than directly mutating the contextvar — direct contextvar
|
``_current_user`` ContextVar is never that end user. The caller instead
|
||||||
operations across pytest test boundaries have produced spooky cross-file
|
carries the real owner in the run request's ``config['context']['user_id']``,
|
||||||
pollution when combined with the langfuse OTel global tracer provider.
|
which ``resolve_runtime_user_id(runtime)`` must prefer over the contextvar —
|
||||||
|
even though conftest's autouse fixture injects ``test-user-autouse`` into it.
|
||||||
"""
|
"""
|
||||||
monkeypatch.setenv("LANGFUSE_TRACING", "true")
|
monkeypatch.setenv("LANGFUSE_TRACING", "true")
|
||||||
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test")
|
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test")
|
||||||
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test")
|
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test")
|
||||||
from deerflow.config.tracing_config import reset_tracing_config
|
from deerflow.config.tracing_config import reset_tracing_config
|
||||||
from deerflow.runtime.runs import worker as worker_module
|
|
||||||
|
reset_tracing_config()
|
||||||
|
|
||||||
|
fake_agent = _FakeAgent()
|
||||||
|
|
||||||
|
def agent_factory(config):
|
||||||
|
return fake_agent
|
||||||
|
|
||||||
|
record = RunRecord(
|
||||||
|
run_id="run-ctx-user",
|
||||||
|
thread_id="thread-ctx",
|
||||||
|
assistant_id="lead-agent",
|
||||||
|
status=RunStatus.pending,
|
||||||
|
on_disconnect=DisconnectMode.cancel,
|
||||||
|
)
|
||||||
|
record.abort_event = asyncio.Event()
|
||||||
|
ctx = RunContext(checkpointer=None)
|
||||||
|
|
||||||
|
await run_agent(
|
||||||
|
_FakeBridge(),
|
||||||
|
_FakeRunManager(),
|
||||||
|
record,
|
||||||
|
ctx=ctx,
|
||||||
|
agent_factory=agent_factory,
|
||||||
|
graph_input={"messages": []},
|
||||||
|
config={
|
||||||
|
"configurable": {"thread_id": "thread-ctx"},
|
||||||
|
"context": {"user_id": "real-end-user"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata = fake_agent.captured_config.get("metadata") or {}
|
||||||
|
# context.user_id wins over the contextvar's ``test-user-autouse``.
|
||||||
|
assert metadata.get("langfuse_user_id") == "real-end-user"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_agent_falls_back_to_default_user_when_unset(monkeypatch):
|
||||||
|
"""When no user is in the contextvar (and no context.user_id), langfuse_user_id
|
||||||
|
falls back to 'default'.
|
||||||
|
|
||||||
|
Uses ``monkeypatch.setattr`` to redirect ``get_effective_user_id`` to return
|
||||||
|
``"default"`` rather than directly mutating the contextvar — direct contextvar
|
||||||
|
operations across pytest test boundaries have produced spooky cross-file
|
||||||
|
pollution when combined with the langfuse OTel global tracer provider.
|
||||||
|
|
||||||
|
The worker resolves the trace user via ``resolve_runtime_user_id(runtime)``;
|
||||||
|
with no ``context.user_id`` it falls back to ``get_effective_user_id()`` — so
|
||||||
|
we patch that fallback at its definition module (``user_context``), which is
|
||||||
|
the name ``resolve_runtime_user_id`` actually calls.
|
||||||
|
"""
|
||||||
|
monkeypatch.setenv("LANGFUSE_TRACING", "true")
|
||||||
|
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-lf-test")
|
||||||
|
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-lf-test")
|
||||||
|
from deerflow.config.tracing_config import reset_tracing_config
|
||||||
|
from deerflow.runtime import user_context as user_context_module
|
||||||
from deerflow.runtime.user_context import DEFAULT_USER_ID
|
from deerflow.runtime.user_context import DEFAULT_USER_ID
|
||||||
|
|
||||||
reset_tracing_config()
|
reset_tracing_config()
|
||||||
monkeypatch.setattr(worker_module, "get_effective_user_id", lambda: DEFAULT_USER_ID)
|
monkeypatch.setattr(user_context_module, "get_effective_user_id", lambda: DEFAULT_USER_ID)
|
||||||
|
|
||||||
fake_agent = _FakeAgent()
|
fake_agent = _FakeAgent()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue