mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-10-12 02:12:31 +00:00
fix(tracing): a process with no terminal answers with the tracing switch
`CREWAI_TRACING_ENABLED=true` — or `tracing=True` — in a script collected an anonymous run and then threw it away. Where nothing can answer the share prompt (CI, a container, an agent), it returns False rather than block for twenty seconds, and for an ephemeral buffer that False was a silent no: the spans were buffered and dropped with an INFO line nobody reads. `tracing_asked_for()` is already the answer the buffer takes before it would prompt. It now holds whether or not a prompt could be shown: the switch is the yes in a terminal and outside one. A suite under test stays a no, a host's consent callback is still consulted first, and the first-time collection nobody asked for still asks — and still declines when nobody answers. The run TUI's own copy of the rule follows. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
58bc1de74a
commit
87b1dc929c
@@ -1235,21 +1235,17 @@ FooterKey .footer-key--key {
|
||||
this worker thread — and a flow never sets it at all. So the declaration
|
||||
is read off the object itself.
|
||||
|
||||
Only ONE of the helper's guards is dropped: it refuses while tracing
|
||||
messages are suppressed, which this app does deliberately to keep
|
||||
crewAI's console out of its own layout, and that guard is about a prompt
|
||||
nobody would see. The others hold. A suite under test, or a TUI with no
|
||||
interactive user behind it — embedded, redirected — has nobody whose yes
|
||||
this could be, and gets the modal's own fail-closed answer instead.
|
||||
The helper's one guard holds: a suite under test is nobody asking to
|
||||
upload. Neither suppressed tracing messages (which this app sets to keep
|
||||
crewAI's console out of its layout) nor a TUI with no interactive user
|
||||
behind it changes the answer — the switch is the yes whether or not a
|
||||
prompt could have been shown.
|
||||
"""
|
||||
import os
|
||||
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
_is_interactive_terminal,
|
||||
_is_test_environment,
|
||||
)
|
||||
from crewai.events.listeners.tracing.utils import _is_test_environment
|
||||
|
||||
if _is_test_environment() or not _is_interactive_terminal():
|
||||
if _is_test_environment():
|
||||
return False
|
||||
|
||||
if os.getenv("CREWAI_TRACING_ENABLED", "").lower() in ("true", "1"):
|
||||
|
||||
@@ -403,12 +403,13 @@ def test_evaluate_says_so_when_this_run_was_not_traced(monkeypatch, why) -> None
|
||||
assert "not traced" in notices[0]
|
||||
|
||||
|
||||
def test_tracing_asked_for_reads_the_declaration_but_keeps_the_other_guards(
|
||||
def test_tracing_asked_for_reads_the_declaration_and_keeps_the_test_guard(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
"""The ContextVar is set where the crew is CONSTRUCTED, not on the worker
|
||||
thread that later asks — but an app with nobody in front of it is still
|
||||
nobody's yes."""
|
||||
thread that later asks — so the declaration is read off the object. With no
|
||||
interactive user behind the app the declaration is still the yes; under test
|
||||
it is nobody's."""
|
||||
utils = "crewai.events.listeners.tracing.utils"
|
||||
monkeypatch.setattr(f"{utils}._is_test_environment", lambda: False)
|
||||
monkeypatch.setattr(f"{utils}._is_interactive_terminal", lambda: True)
|
||||
@@ -421,11 +422,11 @@ def test_tracing_asked_for_reads_the_declaration_but_keeps_the_other_guards(
|
||||
app._crew = SimpleNamespace(tracing=None)
|
||||
assert app._tracing_was_asked_for() is False
|
||||
|
||||
# embedded, redirected, or under test: the declaration is not a person
|
||||
# embedded or redirected: the switch still answers
|
||||
app._crew = SimpleNamespace(tracing=True)
|
||||
monkeypatch.setattr(f"{utils}._is_interactive_terminal", lambda: False)
|
||||
assert app._tracing_was_asked_for() is False
|
||||
monkeypatch.setattr(f"{utils}._is_interactive_terminal", lambda: True)
|
||||
assert app._tracing_was_asked_for() is True
|
||||
# under test: it does not
|
||||
monkeypatch.setattr(f"{utils}._is_test_environment", lambda: True)
|
||||
assert app._tracing_was_asked_for() is False
|
||||
|
||||
|
||||
@@ -107,29 +107,30 @@ def should_suppress_tracing_messages() -> bool:
|
||||
|
||||
|
||||
def tracing_asked_for() -> bool:
|
||||
"""True when a PERSON turned tracing on for this run — `CREWAI_TRACING_ENABLED`,
|
||||
or `tracing=True` on the crew or flow in this context — and is there to have
|
||||
meant it.
|
||||
"""True when somebody turned tracing on for this run — `CREWAI_TRACING_ENABLED`,
|
||||
or `tracing=True` on the crew or flow in this context.
|
||||
|
||||
Turning tracing on is the answer to "may we collect this?"; asking again when
|
||||
the run ends is asking the same question twice. First-time auto-collection is
|
||||
the case that still has to ask, because nobody asked for it.
|
||||
|
||||
Where the prompt could not be shown, there is nothing to answer. A copied
|
||||
`.env` that reaches CI, a container or a server carries the variable without
|
||||
carrying the person; a suite under test, and a host that asked for tracing
|
||||
messages to be suppressed, are the same case. An ephemeral run has always
|
||||
failed closed in all three rather than upload what nobody approved in front
|
||||
of a screen, and it still does — this turns a second question into an
|
||||
answer, and changes nothing about where the first one is asked.
|
||||
That holds where no prompt could be shown too — CI, a container, an agent, a
|
||||
host that suppressed tracing messages. There the prompt returns False rather
|
||||
than block for twenty seconds with nobody to answer it, and for an ephemeral
|
||||
run that False was a silent no: the spans of a run whose tracing the user had
|
||||
explicitly turned on were buffered and then dropped without a word. The
|
||||
switch is the answer, so it is taken as one. A host that must refuse keeps
|
||||
the say through its own consent callback, which is consulted first.
|
||||
|
||||
A suite under test is the one exception: a test that sets the variable is
|
||||
not somebody asking to upload.
|
||||
"""
|
||||
if not (
|
||||
is_tracing_enabled_in_context()
|
||||
or os.getenv("CREWAI_TRACING_ENABLED", "").lower() in ("true", "1")
|
||||
):
|
||||
if _is_test_environment():
|
||||
return False
|
||||
|
||||
return _prompt_can_be_shown()
|
||||
return is_tracing_enabled_in_context() or os.getenv(
|
||||
"CREWAI_TRACING_ENABLED", ""
|
||||
).lower() in ("true", "1")
|
||||
|
||||
|
||||
def should_enable_tracing(*, override: bool | None = None) -> bool:
|
||||
@@ -520,8 +521,8 @@ def _prompt_can_be_shown() -> bool:
|
||||
tracing messages to be suppressed, and a process with no terminal (CI, API
|
||||
servers, Docker) where a prompt would block for twenty seconds with nobody
|
||||
to answer it. `prompt_user_for_trace_viewing` returns False in each, which
|
||||
is what keeps an unapproved ephemeral trace on the machine — and
|
||||
`tracing_asked_for` asks the same question, so the two cannot drift apart.
|
||||
keeps a trace nobody asked for on the machine; a run whose tracing was
|
||||
turned on never reaches the prompt (`tracing_asked_for`).
|
||||
"""
|
||||
if _is_test_environment():
|
||||
return False
|
||||
|
||||
@@ -129,7 +129,7 @@ class EphemeralSpanBuffer(SpanExporter):
|
||||
# an embedder may answer no for reasons of its own. What it replaces
|
||||
# is the PROMPT, and a prompt is what a user who turned tracing on
|
||||
# has already answered: CREWAI_TRACING_ENABLED, or tracing=True, is
|
||||
# the yes. (A callback that opens its own prompt — the TUI's modal —
|
||||
# the yes — with a terminal or without one. (A callback that opens its own prompt — the TUI's modal —
|
||||
# asks `tracing_asked_for()` for itself.)
|
||||
consent = self._consent or _trace_consent.get()
|
||||
approved = (
|
||||
|
||||
@@ -122,8 +122,8 @@ def test_tracing_asked_for_is_the_answer_and_the_run_is_not_asked_again(
|
||||
"""Turning tracing on IS consent. The prompt at the end of a run is for the
|
||||
first-time collection nobody asked for — not for a user who said collect it."""
|
||||
buffers, grants, recorders, prompt = traces
|
||||
# the rule only holds where a prompt could have been shown, and a suite is
|
||||
# one of the places it could not — so say a person is here
|
||||
# a suite under test is the one place the switch is not an answer — so say
|
||||
# this is not one (and that somebody is at a terminal, the ordinary case)
|
||||
utils = "crewai.events.listeners.tracing.utils"
|
||||
monkeypatch.setattr(f"{utils}._is_interactive_terminal", lambda: True)
|
||||
monkeypatch.setattr(f"{utils}._is_test_environment", lambda: False)
|
||||
@@ -145,26 +145,28 @@ def test_tracing_asked_for_is_the_answer_and_the_run_is_not_asked_again(
|
||||
assert grants and recorders # and the trace went where it was told to go
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"closed", ["no terminal", "under test", "messages suppressed"]
|
||||
)
|
||||
def test_where_the_prompt_could_not_be_shown_nothing_is_uploaded(
|
||||
traces, monkeypatch, closed
|
||||
@pytest.mark.parametrize("how", ["env", "flag"])
|
||||
@pytest.mark.parametrize("closed", ["no terminal", "messages suppressed"])
|
||||
def test_where_no_prompt_can_be_shown_the_switch_is_the_answer(
|
||||
traces, monkeypatch, closed, how
|
||||
):
|
||||
"""The switch answers a question; where the question could not have been
|
||||
put to anybody, there is nothing to answer. A copied `.env` reaching CI
|
||||
carries the variable, not the person — and a suite under test, and a host
|
||||
that suppressed tracing messages, are the same case. All three have always
|
||||
failed closed, and still do."""
|
||||
"""CI, a container, an agent: nobody is there to answer the prompt, and it
|
||||
answers False rather than block. For an ephemeral run that False was a
|
||||
silent no — the spans of a run whose tracing the user had explicitly turned
|
||||
on were buffered and then dropped without a word. The switch is the answer
|
||||
here too, whether it was the variable or `tracing=True` in code."""
|
||||
buffers, grants, recorders, prompt = traces
|
||||
monkeypatch.setenv("CREWAI_TRACING_ENABLED", "true")
|
||||
utils = "crewai.events.listeners.tracing.utils"
|
||||
monkeypatch.setattr(f"{utils}._is_interactive_terminal", lambda: closed != "no terminal")
|
||||
monkeypatch.setattr(f"{utils}._is_test_environment", lambda: closed == "under test")
|
||||
monkeypatch.setattr(f"{utils}._is_test_environment", lambda: False)
|
||||
monkeypatch.setattr(
|
||||
f"{utils}.should_suppress_tracing_messages",
|
||||
lambda: closed == "messages suppressed",
|
||||
)
|
||||
if how == "env":
|
||||
monkeypatch.setenv("CREWAI_TRACING_ENABLED", "true")
|
||||
else: # the flag alone must carry it, or this case proves nothing
|
||||
monkeypatch.delenv("CREWAI_TRACING_ENABLED", raising=False)
|
||||
prompt.return_value = False # what the prompt answers where nobody can answer
|
||||
|
||||
class Conversation(Flow):
|
||||
@@ -172,12 +174,60 @@ def test_where_the_prompt_could_not_be_shown_nothing_is_uploaded(
|
||||
def turn(self):
|
||||
return "said in a container"
|
||||
|
||||
assert Conversation(tracing=True).kickoff() == "said in a container"
|
||||
flow = Conversation(tracing=(how == "flag") or None)
|
||||
assert flow.kickoff() == "said in a container"
|
||||
assert crewai_event_bus.flush()
|
||||
|
||||
prompt.assert_not_called()
|
||||
assert grants and recorders # the trace went where it was told to go
|
||||
|
||||
|
||||
def test_under_test_the_switch_uploads_nothing(traces, monkeypatch):
|
||||
"""A suite that sets the variable is not somebody asking to upload."""
|
||||
buffers, grants, recorders, prompt = traces
|
||||
monkeypatch.setenv("CREWAI_TRACING_ENABLED", "true")
|
||||
utils = "crewai.events.listeners.tracing.utils"
|
||||
monkeypatch.setattr(f"{utils}._is_interactive_terminal", lambda: False)
|
||||
monkeypatch.setattr(f"{utils}._is_test_environment", lambda: True)
|
||||
prompt.return_value = False
|
||||
|
||||
class Conversation(Flow):
|
||||
@start()
|
||||
def turn(self):
|
||||
return "said under test"
|
||||
|
||||
assert Conversation(tracing=True).kickoff() == "said under test"
|
||||
assert crewai_event_bus.flush()
|
||||
|
||||
assert not grants and not recorders # nothing left the machine
|
||||
|
||||
|
||||
def test_tracing_nobody_asked_for_is_still_a_no_without_a_terminal(
|
||||
traces, monkeypatch
|
||||
):
|
||||
"""The first-time collection turns tracing on by itself; nobody asked, so
|
||||
with nobody to answer, nothing is uploaded."""
|
||||
buffers, grants, recorders, prompt = traces
|
||||
monkeypatch.delenv("CREWAI_TRACING_ENABLED", raising=False)
|
||||
utils = "crewai.events.listeners.tracing.utils"
|
||||
monkeypatch.setattr(f"{utils}._is_interactive_terminal", lambda: False)
|
||||
monkeypatch.setattr(f"{utils}._is_test_environment", lambda: False)
|
||||
monkeypatch.setattr(f"{utils}.is_tracing_enabled_in_context", lambda: False)
|
||||
written = Mock()
|
||||
monkeypatch.setattr("crewai.telemetry.tracing.ephemeral.update_user_data", written)
|
||||
prompt.return_value = False
|
||||
|
||||
buffer = EphemeralSpanBuffer()
|
||||
buffer._spans.append((object(), 1))
|
||||
buffer.share("run-nobody-asked-for", first_time=True)
|
||||
|
||||
prompt.assert_called_once()
|
||||
written.assert_called_once_with(
|
||||
{"first_execution_done": True, "trace_consent": False}
|
||||
)
|
||||
assert not grants and not recorders
|
||||
|
||||
|
||||
def test_enabling_deferred_trace_opens_a_new_flow_root(traces):
|
||||
buffers, grants, recorders, prompt = traces
|
||||
sessions = []
|
||||
|
||||
Reference in New Issue
Block a user