mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
Two related changes aimed at the OSS -> AMP bridge for projects built with AI coding assistants. AGENTS.md (copied into every `crewai create` project): - Add a "when to bring it up" section near the top with explicit trigger conditions for traces and deployment, phrased for coding assistants. - Add a full "Observability & Traces" section. Traces previously appeared once in 1018 lines despite being the zero-friction entry point; they now appear throughout, including that no account is required and that unauthenticated runs get a 24h ephemeral link. - Note that deploying is free to get online and scales vertically and horizontally, so an agent suggests it instead of hand-rolling a Dockerfile, server, and scheduler. - Add traces commands to the Quick Reference and two entries to Best Practices. - Guidance is conditional and truthful: mention once when a trigger actually fires, then drop it. Telemetry: - Add `detect_coding_agent()`, which identifies the AI coding assistant running the process from environment markers (Claude Code, Cursor, Codex, Gemini CLI, Aider, Windsurf, Devin, Replit, Copilot, OpenHands, Cline, Amp), falling back to editor-terminal hints and then to non_interactive/unknown. - Record it as a `coding_agent` attribute on Crew Created and Flow Creation spans, and emit `coding_agent:<name>` once per process as a feature usage event so it lands in the existing aggregation with no new pipeline work. - Only the normalized assistant name is ever recorded; environment variable values are never read into the result. This gives us the data to size how much of CrewAI is now authored by coding agents, and which ones, before investing further in that channel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t
232 lines
7.5 KiB
Python
232 lines
7.5 KiB
Python
import os
|
|
import threading
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
from crewai import Agent, Crew, Task
|
|
from crewai.telemetry import Telemetry
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def cleanup_telemetry():
|
|
Telemetry._instance = None
|
|
if hasattr(Telemetry, "_lock"):
|
|
Telemetry._lock = threading.Lock()
|
|
yield
|
|
Telemetry._instance = None
|
|
if hasattr(Telemetry, "_lock"):
|
|
Telemetry._lock = threading.Lock()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"env_var,value,expected_ready",
|
|
[
|
|
("OTEL_SDK_DISABLED", "true", False),
|
|
("OTEL_SDK_DISABLED", "TRUE", False),
|
|
("CREWAI_DISABLE_TELEMETRY", "true", False),
|
|
("CREWAI_DISABLE_TELEMETRY", "TRUE", False),
|
|
("OTEL_SDK_DISABLED", "false", True),
|
|
("CREWAI_DISABLE_TELEMETRY", "false", True),
|
|
],
|
|
)
|
|
def test_telemetry_environment_variables(env_var, value, expected_ready):
|
|
"""Test telemetry state with different environment variable configurations."""
|
|
# Clear all telemetry-related env vars first, then set only the one being tested
|
|
env_overrides = {
|
|
"OTEL_SDK_DISABLED": "false",
|
|
"CREWAI_DISABLE_TELEMETRY": "false",
|
|
"CREWAI_DISABLE_TRACKING": "false",
|
|
env_var: value,
|
|
}
|
|
with patch.dict(os.environ, env_overrides):
|
|
with patch("crewai.telemetry.telemetry.TracerProvider"):
|
|
telemetry = Telemetry()
|
|
assert telemetry.ready is expected_ready
|
|
|
|
|
|
def test_telemetry_enabled_by_default():
|
|
"""Test that telemetry is enabled by default."""
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
with patch("crewai.telemetry.telemetry.TracerProvider"):
|
|
telemetry = Telemetry()
|
|
assert telemetry.ready is True
|
|
|
|
|
|
def test_set_tracer_skips_when_provider_already_configured():
|
|
"""A second telemetry instance must not re-install the global provider."""
|
|
with (
|
|
patch.dict(os.environ, {}, clear=True),
|
|
patch(
|
|
"crewai.telemetry.telemetry.trace.get_tracer_provider",
|
|
return_value=TracerProvider(),
|
|
),
|
|
patch("crewai.telemetry.telemetry.trace.set_tracer_provider") as mock_set,
|
|
):
|
|
telemetry = Telemetry()
|
|
telemetry.set_tracer()
|
|
|
|
mock_set.assert_not_called()
|
|
assert telemetry.trace_set is True
|
|
|
|
|
|
def test_flow_execution_span_records_crewai_version():
|
|
tracer = Mock()
|
|
span = Mock()
|
|
tracer.start_span.return_value = span
|
|
|
|
with (
|
|
patch.dict(
|
|
os.environ,
|
|
{
|
|
"CREWAI_DISABLE_TELEMETRY": "false",
|
|
"CREWAI_DISABLE_TRACKING": "false",
|
|
"OTEL_SDK_DISABLED": "false",
|
|
},
|
|
),
|
|
patch("crewai.telemetry.telemetry.TracerProvider"),
|
|
patch("crewai.telemetry.telemetry.trace.get_tracer", return_value=tracer),
|
|
patch("crewai.telemetry.telemetry.version", return_value="9.9.9"),
|
|
):
|
|
telemetry = Telemetry()
|
|
telemetry.flow_execution_span("ResearchFlow", ["start", "finish"])
|
|
|
|
tracer.start_span.assert_called_once_with("Flow Execution")
|
|
span.set_attribute.assert_any_call("crewai_version", "9.9.9")
|
|
span.set_attribute.assert_any_call("flow_name", "ResearchFlow")
|
|
|
|
|
|
def test_flow_creation_span_records_crewai_version():
|
|
tracer = Mock()
|
|
span = Mock()
|
|
tracer.start_span.return_value = span
|
|
|
|
with (
|
|
patch.dict(
|
|
os.environ,
|
|
{
|
|
"CREWAI_DISABLE_TELEMETRY": "false",
|
|
"CREWAI_DISABLE_TRACKING": "false",
|
|
"OTEL_SDK_DISABLED": "false",
|
|
},
|
|
),
|
|
patch("crewai.telemetry.telemetry.TracerProvider"),
|
|
patch("crewai.telemetry.telemetry.trace.get_tracer", return_value=tracer),
|
|
patch("crewai.telemetry.telemetry.version", return_value="9.9.9"),
|
|
):
|
|
telemetry = Telemetry()
|
|
# Flow creation also emits a once-per-process coding_agent feature span.
|
|
telemetry._coding_agent_reported = True
|
|
telemetry.flow_creation_span("ResearchFlow")
|
|
|
|
tracer.start_span.assert_called_once_with("Flow Creation")
|
|
span.set_attribute.assert_any_call("crewai_version", "9.9.9")
|
|
span.set_attribute.assert_any_call("flow_name", "ResearchFlow")
|
|
|
|
|
|
@patch("crewai.telemetry.telemetry.logger.error")
|
|
@patch(
|
|
"opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter.export",
|
|
side_effect=Exception("Test exception"),
|
|
)
|
|
@pytest.mark.vcr()
|
|
def test_telemetry_fails_due_connect_timeout(export_mock, logger_mock):
|
|
error = Exception("Test exception")
|
|
export_mock.side_effect = error
|
|
|
|
with patch.dict(
|
|
os.environ, {"CREWAI_DISABLE_TELEMETRY": "false", "OTEL_SDK_DISABLED": "false"}
|
|
):
|
|
telemetry = Telemetry()
|
|
|
|
tracer = telemetry.provider.get_tracer(__name__)
|
|
with tracer.start_as_current_span("test-span"):
|
|
agent = Agent(
|
|
role="agent",
|
|
llm="gpt-4o-mini",
|
|
goal="Just say hi",
|
|
backstory="You are a helpful assistant that just says hi",
|
|
)
|
|
task = Task(
|
|
description="Just say hi",
|
|
expected_output="hi",
|
|
agent=agent,
|
|
)
|
|
crew = Crew(agents=[agent], tasks=[task], name="TestCrew")
|
|
crew.kickoff()
|
|
|
|
telemetry.provider.force_flush()
|
|
|
|
assert export_mock.called
|
|
assert logger_mock.call_count == export_mock.call_count
|
|
for call in logger_mock.call_args_list:
|
|
assert call[0][0] == error
|
|
|
|
|
|
@pytest.mark.telemetry
|
|
def test_telemetry_singleton_pattern():
|
|
"""Test that Telemetry uses the singleton pattern correctly."""
|
|
Telemetry._instance = None
|
|
|
|
telemetry1 = Telemetry()
|
|
telemetry2 = Telemetry()
|
|
|
|
assert telemetry1 is telemetry2
|
|
|
|
telemetry1.test_attribute = "test_value"
|
|
assert hasattr(telemetry2, "test_attribute")
|
|
assert telemetry2.test_attribute == "test_value"
|
|
|
|
import threading
|
|
|
|
instances = []
|
|
|
|
def create_instance():
|
|
instances.append(Telemetry())
|
|
|
|
threads = [threading.Thread(target=create_instance) for _ in range(5)]
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join()
|
|
|
|
assert all(instance is telemetry1 for instance in instances)
|
|
|
|
|
|
def test_no_signal_handler_traceback_in_non_main_thread():
|
|
"""Signal handler registration should be silently skipped in non-main threads.
|
|
|
|
Regression test for https://github.com/crewAIInc/crewAI/issues/4289
|
|
"""
|
|
errors: list[Exception] = []
|
|
mock_holder: dict = {}
|
|
|
|
def init_in_thread():
|
|
try:
|
|
Telemetry._instance = None
|
|
with (
|
|
patch.dict(
|
|
os.environ,
|
|
{"CREWAI_DISABLE_TELEMETRY": "false", "OTEL_SDK_DISABLED": "false"},
|
|
),
|
|
patch("crewai.telemetry.telemetry.TracerProvider"),
|
|
patch("signal.signal") as mock_signal,
|
|
patch("crewai.telemetry.telemetry.logger") as mock_logger,
|
|
):
|
|
Telemetry()
|
|
mock_holder["signal"] = mock_signal
|
|
mock_holder["logger"] = mock_logger
|
|
except Exception as exc:
|
|
errors.append(exc)
|
|
|
|
thread = threading.Thread(target=init_in_thread)
|
|
thread.start()
|
|
thread.join()
|
|
|
|
assert not errors, f"Unexpected error: {errors}"
|
|
assert mock_holder, "Thread did not execute"
|
|
mock_holder["signal"].assert_not_called()
|
|
mock_holder["logger"].debug.assert_any_call(
|
|
"Skipping signal handler registration: not running in main thread"
|
|
)
|