5a558eb09e
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
336 lines
10 KiB
Python
336 lines
10 KiB
Python
"""Unit tests for the SDK ``environment`` plumbing.
|
|
|
|
Covers:
|
|
- ``Opik.trace(environment=...)`` is the only entry point that accepts an
|
|
explicit environment; the value flows into the emitted ``CreateTraceMessage``.
|
|
- Spans (``Trace.span``, ``Span.span``, ``@track``-created spans) inherit the
|
|
parent trace's environment unconditionally.
|
|
- A nested ``@track(environment=...)`` whose value differs from the enclosing
|
|
trace's environment logs a warning and is ignored — the parent trace's value
|
|
wins (mirrors how mismatched ``project_name`` is handled).
|
|
- Backwards compat: existing call sites without ``environment`` still work.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import opik
|
|
from opik.api_objects import opik_client
|
|
from opik.message_processing import messages
|
|
from opik import dict_utils
|
|
|
|
|
|
def _capture_messages(client: opik_client.Opik) -> MagicMock:
|
|
mock_streamer = MagicMock()
|
|
client._streamer = mock_streamer
|
|
return mock_streamer
|
|
|
|
|
|
def _create_trace_messages(streamer: MagicMock):
|
|
return [
|
|
c.args[0]
|
|
for c in streamer.put.call_args_list
|
|
if isinstance(c.args[0], messages.CreateTraceMessage)
|
|
]
|
|
|
|
|
|
def _create_span_messages(streamer: MagicMock):
|
|
return [
|
|
c.args[0]
|
|
for c in streamer.put.call_args_list
|
|
if isinstance(c.args[0], messages.CreateSpanMessage)
|
|
]
|
|
|
|
|
|
def test_opik_client__no_environment_set__messages_have_none_environment_and_payload_omits_it():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
|
|
client.trace(name="t")
|
|
client.span(name="s")
|
|
|
|
trace_msg = _create_trace_messages(streamer)[0]
|
|
span_msg = _create_span_messages(streamer)[0]
|
|
|
|
assert trace_msg.environment is None
|
|
assert span_msg.environment is None
|
|
|
|
cleaned = dict_utils.remove_none_from_dict(trace_msg.as_payload_dict())
|
|
assert "environment" not in cleaned
|
|
|
|
|
|
def test_opik_client_trace__environment_kwarg_propagates_to_create_trace_message():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
|
|
client.trace(name="t", environment="staging")
|
|
|
|
assert _create_trace_messages(streamer)[0].environment == "staging"
|
|
|
|
|
|
def test_opik_client_trace__span_inherits_trace_environment():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
|
|
trace = client.trace(name="t", environment="staging")
|
|
trace.span(name="s")
|
|
|
|
assert _create_span_messages(streamer)[0].environment == "staging"
|
|
|
|
|
|
def test_opik_client_trace__nested_span_inherits_environment():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
|
|
trace = client.trace(name="t", environment="staging")
|
|
span = trace.span(name="parent-span")
|
|
span.span(name="child-span")
|
|
|
|
span_msgs = _create_span_messages(streamer)
|
|
assert len(span_msgs) == 2
|
|
assert all(m.environment == "staging" for m in span_msgs)
|
|
|
|
|
|
def test_track_decorator__environment_propagates_to_root_trace_and_spans():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
opik_client.set_global_client(client)
|
|
|
|
@opik.track(environment="staging")
|
|
def my_fn():
|
|
return 42
|
|
|
|
my_fn()
|
|
|
|
trace_msgs = _create_trace_messages(streamer)
|
|
span_msgs = _create_span_messages(streamer)
|
|
assert trace_msgs and span_msgs
|
|
assert all(m.environment == "staging" for m in trace_msgs)
|
|
assert all(m.environment == "staging" for m in span_msgs)
|
|
|
|
|
|
def test_track_decorator__nested_environment_mismatch_warns_and_inherits_parent(
|
|
monkeypatch,
|
|
):
|
|
from opik.api_objects import helpers as opik_helpers
|
|
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
opik_client.set_global_client(client)
|
|
|
|
warnings = []
|
|
monkeypatch.setattr(
|
|
opik_helpers.LOGGER,
|
|
"warning",
|
|
lambda msg, *a, **kw: warnings.append(msg % a if a else msg),
|
|
)
|
|
|
|
@opik.track(environment="this-should-be-ignored")
|
|
def inner():
|
|
return 1
|
|
|
|
@opik.track(environment="staging")
|
|
def outer():
|
|
return inner()
|
|
|
|
outer()
|
|
|
|
span_msgs = _create_span_messages(streamer)
|
|
assert span_msgs
|
|
assert all(m.environment == "staging" for m in span_msgs)
|
|
assert any("this-should-be-ignored" in w and "staging" in w for w in warnings), (
|
|
warnings
|
|
)
|
|
|
|
|
|
def test_create_trace_message__environment_field_default_is_none_for_backwards_compat():
|
|
msg = messages.CreateTraceMessage(
|
|
trace_id="t",
|
|
project_name="p",
|
|
name=None,
|
|
start_time=None, # type: ignore[arg-type]
|
|
end_time=None,
|
|
input=None,
|
|
output=None,
|
|
metadata=None,
|
|
tags=None,
|
|
error_info=None,
|
|
thread_id=None,
|
|
last_updated_at=None,
|
|
source="sdk",
|
|
)
|
|
assert msg.environment is None
|
|
|
|
|
|
def test_update_environment__colour_on_builtin_raises_error():
|
|
from opik.exceptions import EnvironmentConfigurationError
|
|
|
|
client = opik_client.Opik(project_name="test-project")
|
|
|
|
for env_name in ("production", "staging", "development"):
|
|
try:
|
|
client.update_environment(env_name, color="#ff0000")
|
|
assert False, f"expected EnvironmentConfigurationError for {env_name!r}"
|
|
except EnvironmentConfigurationError as e:
|
|
assert env_name in str(e)
|
|
|
|
|
|
def test_update_environment__colour_on_builtin_not_called_when_no_colour():
|
|
from unittest.mock import patch
|
|
from opik.rest_api.types.environment_public import EnvironmentPublic
|
|
|
|
client = opik_client.Opik(project_name="test-project")
|
|
|
|
fake_env = EnvironmentPublic(id="abc", name="production")
|
|
with (
|
|
patch.object(client, "_find_environment_by_name", return_value=fake_env),
|
|
patch.object(
|
|
client._rest_client.environments,
|
|
"update_environment",
|
|
return_value=None,
|
|
) as mock_update,
|
|
patch.object(
|
|
client._rest_client.environments,
|
|
"get_environment_by_id",
|
|
return_value=fake_env,
|
|
),
|
|
):
|
|
result = client.update_environment("production", description="new desc")
|
|
mock_update.assert_called_once()
|
|
assert result == fake_env
|
|
|
|
|
|
def test_update_environment__colour_on_custom_env_is_allowed():
|
|
from unittest.mock import patch
|
|
from opik.rest_api.types.environment_public import EnvironmentPublic
|
|
|
|
client = opik_client.Opik(project_name="test-project")
|
|
|
|
fake_env = EnvironmentPublic(id="xyz", name="my-custom-env")
|
|
with (
|
|
patch.object(client, "_find_environment_by_name", return_value=fake_env),
|
|
patch.object(
|
|
client._rest_client.environments,
|
|
"update_environment",
|
|
return_value=None,
|
|
) as mock_update,
|
|
patch.object(
|
|
client._rest_client.environments,
|
|
"get_environment_by_id",
|
|
return_value=fake_env,
|
|
),
|
|
):
|
|
result = client.update_environment("my-custom-env", color="#123456")
|
|
assert result == fake_env
|
|
mock_update.assert_called_once()
|
|
assert mock_update.call_args.kwargs.get("color") == "#123456"
|
|
|
|
|
|
def test_create_environment__conflict_raises_environment_already_exists():
|
|
from unittest.mock import patch
|
|
from opik.exceptions import EnvironmentAlreadyExists
|
|
from opik.rest_api.errors import ConflictError
|
|
|
|
client = opik_client.Opik(project_name="test-project")
|
|
|
|
with patch.object(
|
|
client._rest_client.environments,
|
|
"create_environment",
|
|
side_effect=ConflictError(body={"message": "already exists"}),
|
|
):
|
|
try:
|
|
client.create_environment("production")
|
|
assert False, "expected EnvironmentAlreadyExists"
|
|
except EnvironmentAlreadyExists as e:
|
|
assert "production" in str(e)
|
|
|
|
|
|
def test_create_span_message__environment_field_default_is_none_for_backwards_compat():
|
|
msg = messages.CreateSpanMessage(
|
|
span_id="s",
|
|
trace_id="t",
|
|
project_name="p",
|
|
parent_span_id=None,
|
|
name=None,
|
|
start_time=None, # type: ignore[arg-type]
|
|
end_time=None,
|
|
input=None,
|
|
output=None,
|
|
metadata=None,
|
|
tags=None,
|
|
type="general",
|
|
usage=None,
|
|
model=None,
|
|
provider=None,
|
|
error_info=None,
|
|
total_cost=None,
|
|
last_updated_at=None,
|
|
source="sdk",
|
|
)
|
|
assert msg.environment is None
|
|
|
|
|
|
def _update_span_messages(streamer: MagicMock):
|
|
return [
|
|
c.args[0]
|
|
for c in streamer.put.call_args_list
|
|
if isinstance(c.args[0], messages.UpdateSpanMessage)
|
|
]
|
|
|
|
|
|
def _update_trace_messages(streamer: MagicMock):
|
|
return [
|
|
c.args[0]
|
|
for c in streamer.put.call_args_list
|
|
if isinstance(c.args[0], messages.UpdateTraceMessage)
|
|
]
|
|
|
|
|
|
def test_span_end__preserves_environment_in_update_message():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
|
|
trace = client.trace(name="t", environment="staging")
|
|
span = trace.span(name="s")
|
|
span.end()
|
|
|
|
update_msgs = _update_span_messages(streamer)
|
|
assert len(update_msgs) == 1
|
|
assert update_msgs[0].environment == "staging"
|
|
|
|
|
|
def test_span_update__preserves_environment_in_update_message():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
|
|
trace = client.trace(name="t", environment="production")
|
|
span = trace.span(name="s")
|
|
span.update(output={"result": "ok"})
|
|
|
|
update_msgs = _update_span_messages(streamer)
|
|
assert len(update_msgs) == 1
|
|
assert update_msgs[0].environment == "production"
|
|
|
|
|
|
def test_trace_end__preserves_environment_in_update_message():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
|
|
trace = client.trace(name="t", environment="staging")
|
|
trace.end()
|
|
|
|
update_msgs = _update_trace_messages(streamer)
|
|
assert len(update_msgs) == 1
|
|
assert update_msgs[0].environment == "staging"
|
|
|
|
|
|
def test_trace_update__preserves_environment_in_update_message():
|
|
client = opik_client.Opik(project_name="test-project")
|
|
streamer = _capture_messages(client)
|
|
|
|
trace = client.trace(name="t", environment="production")
|
|
trace.update(output={"result": "ok"})
|
|
|
|
update_msgs = _update_trace_messages(streamer)
|
|
assert len(update_msgs) == 1
|
|
assert update_msgs[0].environment == "production"
|