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:
d33kayyy 2026-07-04 23:07:27 +07:00 committed by GitHub
parent 4d660b202a
commit 84bbdf6e00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 66 additions and 10 deletions

View file

@ -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"),

View file

@ -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()