Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

986 lines
35 KiB
Python

from __future__ import annotations
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from config.constants import SENTRY_DSN, SENTRY_ERROR_SAMPLE_RATE, SENTRY_TRACES_SAMPLE_RATE
from platform.observability.errors import sentry as sentry_mod
_REAL_BUILD_INTEGRATIONS = sentry_mod._build_sentry_integrations
@pytest.fixture(autouse=True)
def _reset_sentry_module_state(monkeypatch: pytest.MonkeyPatch) -> None:
"""Clear cached init/tag state, and skip building real integrations.
The default test pattern replaces ``sys.modules["sentry_sdk"]`` with a
``SimpleNamespace`` stub, which breaks ``from sentry_sdk.integrations.X
import Y`` because the stub is not a package. Stubbing the integrations
builder with an empty list keeps every test working; the one test that
asserts integrations were wired overrides this in its own body.
Also clears the global ``OPENSRE_SENTRY_DISABLED`` flag set by conftest so
that ``_before_send`` unit tests can exercise the scrubbing/filtering logic.
Tests that need the disable flag re-set it via ``monkeypatch.setenv``.
"""
sentry_mod._init_sentry_once.cache_clear()
sentry_mod._reset_scope_tags_state_for_tests()
monkeypatch.setattr(sentry_mod, "_build_sentry_integrations", lambda: [])
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
def test_init_sentry_noops_when_disabled(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.setenv("OPENSRE_SENTRY_DISABLED", "1")
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
init_mock.assert_not_called()
def test_init_sentry_is_idempotent_for_same_config(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
monkeypatch.delenv("OPENSRE_SENTRY_DSN", raising=False)
monkeypatch.delenv("SENTRY_DSN", raising=False)
monkeypatch.setenv("SENTRY_ERROR_SAMPLE_RATE", "0.25")
monkeypatch.setenv("SENTRY_TRACES_SAMPLE_RATE", "0.5")
monkeypatch.setenv("ENV", "production")
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
sentry_mod.init_sentry()
init_mock.assert_called_once()
assert init_mock.call_args.kwargs["dsn"] == SENTRY_DSN
assert init_mock.call_args.kwargs["environment"] == "production"
assert init_mock.call_args.kwargs["send_default_pii"] is False
assert init_mock.call_args.kwargs["attach_stacktrace"] is True
assert init_mock.call_args.kwargs["sample_rate"] == 0.25
assert init_mock.call_args.kwargs["traces_sample_rate"] == 0.5
def test_init_sentry_allows_dsn_override(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
monkeypatch.setenv("OPENSRE_SENTRY_DSN", "https://override@sentry.invalid/1")
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
assert init_mock.call_args.kwargs["dsn"] == "https://override@sentry.invalid/1"
def test_init_sentry_invalid_sample_rate_fallbacks(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
monkeypatch.delenv("OPENSRE_SENTRY_DSN", raising=False)
monkeypatch.delenv("SENTRY_DSN", raising=False)
monkeypatch.setenv("SENTRY_ERROR_SAMPLE_RATE", "invalid_value")
monkeypatch.setenv("SENTRY_TRACES_SAMPLE_RATE", "invalid_value")
monkeypatch.setenv("ENV", "production")
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
init_mock.assert_called_once()
assert init_mock.call_args.kwargs["sample_rate"] == SENTRY_ERROR_SAMPLE_RATE
assert init_mock.call_args.kwargs["traces_sample_rate"] == SENTRY_TRACES_SAMPLE_RATE
def test_init_sentry_sample_rates_are_clamped(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
monkeypatch.delenv("OPENSRE_SENTRY_DSN", raising=False)
monkeypatch.delenv("SENTRY_DSN", raising=False)
monkeypatch.setenv("SENTRY_ERROR_SAMPLE_RATE", "2")
monkeypatch.setenv("SENTRY_TRACES_SAMPLE_RATE", "-1")
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
assert init_mock.call_args.kwargs["sample_rate"] == 1.0
assert init_mock.call_args.kwargs["traces_sample_rate"] == 0.0
def test_capture_exception_is_best_effort(monkeypatch) -> None:
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
capture_mock = MagicMock(side_effect=RuntimeError("sentry unavailable"))
monkeypatch.setitem(
sys.modules,
"sentry_sdk",
SimpleNamespace(capture_exception=capture_mock),
)
sentry_mod.capture_exception(ValueError("boom"))
capture_mock.assert_called_once()
def test_capture_exception_attaches_context(monkeypatch) -> None:
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
capture_mock = MagicMock()
tags: dict[str, str] = {}
extras: dict[str, object] = {}
class _Scope:
def __enter__(self) -> _Scope:
return self
def __exit__(self, *_args: object) -> None:
return None
def set_tag(self, key: str, value: str) -> None:
tags[key] = value
def set_extra(self, key: str, value: object) -> None:
extras[key] = value
monkeypatch.setitem(
sys.modules,
"sentry_sdk",
SimpleNamespace(capture_exception=capture_mock, push_scope=_Scope),
)
sentry_mod.capture_exception(
ValueError("boom"),
context="surfaces.interactive_shell.cli_agent.stream",
extra={"turn": 3},
tags={"surface": "interactive_shell"},
)
capture_mock.assert_called_once()
assert tags == {
"opensre.context": "surfaces.interactive_shell.cli_agent.stream",
"surface": "interactive_shell",
}
assert extras == {"turn": 3}
def test_init_sentry_noops_when_opensre_no_telemetry(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.setenv("OPENSRE_NO_TELEMETRY", "1")
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
init_mock.assert_not_called()
def test_init_sentry_noops_when_do_not_track(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.setenv("DO_NOT_TRACK", "1")
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
init_mock.assert_not_called()
def test_init_sentry_dsn_env_overrides_constant(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
custom_dsn = "https://abc@example.ingest.sentry.io/12345"
monkeypatch.setenv("SENTRY_DSN", custom_dsn)
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
assert init_mock.call_args.kwargs["dsn"] == custom_dsn
def test_init_sentry_release_tag_uses_get_opensre_version(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
monkeypatch.delenv("OPENSRE_SENTRY_DISABLED", raising=False)
monkeypatch.delenv("OPENSRE_NO_TELEMETRY", raising=False)
monkeypatch.delenv("DO_NOT_TRACK", raising=False)
monkeypatch.setattr("config.version.get_opensre_version", lambda: "9.9.9")
init_mock = MagicMock()
monkeypatch.setitem(sys.modules, "sentry_sdk", SimpleNamespace(init=init_mock))
sentry_mod.init_sentry()
assert init_mock.call_args.kwargs["release"] == "opensre@9.9.9"
def test_before_send_filters_sensitive_request_headers() -> None:
event = {
"request": {
"headers": {
"Authorization": "Bearer secret-token",
"Cookie": "session=abc",
"User-Agent": "opensre/1",
},
"cookies": {"session": "abc"},
},
}
sentry_mod._before_send(event, {})
headers = event["request"]["headers"]
assert headers["Authorization"] == "[Filtered]"
assert headers["Cookie"] == "[Filtered]"
assert headers["User-Agent"] == "opensre/1"
assert event["request"]["cookies"] == "[Filtered]"
def test_before_send_drops_event_when_dsn_is_empty(monkeypatch) -> None:
monkeypatch.setenv("SENTRY_DSN", "")
monkeypatch.setattr(sentry_mod, "SENTRY_DSN", "")
assert sentry_mod._before_send({"message": "boom"}, {}) is None
def test_before_send_drops_event_when_sentry_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("OPENSRE_SENTRY_DISABLED", "1")
assert sentry_mod._before_send({"message": "boom"}, {}) is None
def test_before_send_filters_sensitive_extra_keys() -> None:
event = {
"extra": {
"github_token": "ghp_xxx",
"api_key": "abc",
"user_email": "user@example.com",
},
}
sentry_mod._before_send(event, {})
assert event["extra"]["github_token"] == "[Filtered]"
assert event["extra"]["api_key"] == "[Filtered]"
assert event["extra"]["user_email"] == "user@example.com"
def test_before_send_fingerprints_tool_errors_by_tool_name() -> None:
event = {"tags": {"tool": "grafana_logs"}, "message": "tool failed"}
sentry_mod._before_send(event, {})
assert event["fingerprint"] == ["tool-error", "grafana_logs", "{{ default }}"]
def test_before_send_fingerprints_node_errors_by_node_name() -> None:
event = {"tags": {"node": "extract_alert"}, "message": "node failed"}
sentry_mod._before_send(event, {})
assert event["fingerprint"] == ["node-error", "extract_alert", "{{ default }}"]
def test_before_send_prefers_tool_fingerprint_when_tool_and_node_tags_exist() -> None:
event = {
"tags": {"tool": "grafana_logs", "node": "investigate"},
"message": "tool failed inside node",
}
sentry_mod._before_send(event, {})
assert event["fingerprint"] == ["tool-error", "grafana_logs", "{{ default }}"]
def test_before_send_scrubs_home_paths_in_stack_frames() -> None:
event = {
"exception": {
"values": [
{
"stacktrace": {
"frames": [
{
"abs_path": "/Users/jane/project/app/foo.py",
"vars": {
"path": "/home/runner/secret",
"auth_token": "ghp_xxx",
},
}
]
}
}
]
}
}
sentry_mod._before_send(event, {})
frame = event["exception"]["values"][0]["stacktrace"]["frames"][0]
assert frame["abs_path"] == "~/project/app/foo.py"
assert frame["vars"]["path"] == "~/secret"
assert frame["vars"]["auth_token"] == "[Filtered]"
def test_before_send_scrubs_pydantic_input_value_from_exception_message() -> None:
"""Pydantic V2 ValidationError renders ``input_value=<raw>`` inside the
exception message. When the failing field is a secret (api_token,
password, ...), the raw value lands in ``exception.values[].value`` and
bypasses the key-based scrubbers. ``_scrub_event_in_place`` strips it
before transport.
"""
event = {
"exception": {
"values": [
{
"type": "ValidationError",
"value": (
"1 validation error for VercelConfig\n"
"api_token\n"
" String should match pattern '...' "
"[type=string_pattern_mismatch, "
"input_value='sk-real-secret-token-xyz', "
"input_type=str]"
),
}
]
}
}
sentry_mod._before_send(event, {})
scrubbed = event["exception"]["values"][0]["value"]
assert "sk-real-secret-token-xyz" not in scrubbed
assert "input_value=[Filtered]" in scrubbed
# Surrounding triage context must survive — only the value is stripped.
assert "ValidationError" in event["exception"]["values"][0]["type"]
assert "api_token" in scrubbed
assert "string_pattern_mismatch" in scrubbed
def test_before_send_scrubs_input_value_across_multiple_errors() -> None:
# Multi-field ValidationError: every input_value occurrence must be stripped.
event = {
"exception": {
"values": [
{
"type": "ValidationError",
"value": (
"2 validation errors for Demo\n"
"api_key\n"
" String should have at least 10 characters "
"[type=string_too_short, input_value='leaky-api-key', input_type=str]\n"
"password\n"
" Input should be a valid string "
"[type=string_type, input_value='p4ssword!', input_type=str]"
),
}
]
}
}
sentry_mod._before_send(event, {})
scrubbed = event["exception"]["values"][0]["value"]
assert "leaky-api-key" not in scrubbed
assert "p4ssword!" not in scrubbed
assert scrubbed.count("input_value=[Filtered]") == 2
def test_before_send_leaves_unrelated_exception_messages_alone() -> None:
# A non-pydantic message must pass through verbatim (modulo home-path
# substitution which is the existing behaviour for strings).
original = "ConnectionError: cannot connect to 10.0.0.1:5432"
event = {"exception": {"values": [{"type": "ConnectionError", "value": original}]}}
sentry_mod._before_send(event, {})
assert event["exception"]["values"][0]["value"] == original
def test_before_send_scrubs_list_typed_input_value_without_mid_clip() -> None:
# Pydantic V2 flattens list-field errors to per-element scalar entries
# (val.0, val.1, ...), so the rendered ``input_value=`` is always a
# scalar repr. Lock that behaviour: the scrubber must replace the whole
# value, not clip an inner bracket.
event = {
"exception": {
"values": [
{
"type": "ValidationError",
"value": (
"2 validation errors for A\n"
"val.0\n Input should be a valid integer "
"[type=int_parsing, input_value='secret_a', input_type=str]\n"
"val.1\n Input should be a valid integer "
"[type=int_parsing, input_value='secret_b', input_type=str]"
),
}
]
}
}
sentry_mod._before_send(event, {})
scrubbed = event["exception"]["values"][0]["value"]
assert "secret_a" not in scrubbed
assert "secret_b" not in scrubbed
assert scrubbed.count("input_value=[Filtered]") == 2
def test_scrub_exception_value_is_idempotent() -> None:
# Re-scrubbing an already-scrubbed message must be a no-op. Guards
# against a future refactor that runs the scrub pass twice (e.g. once
# in before_send and once in a downstream hook).
once = sentry_mod._scrub_exception_value(
"[type=string_too_short, input_value='leaky', input_type=str]"
)
twice = sentry_mod._scrub_exception_value(once)
assert once == twice
assert "leaky" not in once
def test_scrub_exception_value_handles_truncated_message_without_input_type() -> None:
# Defense in depth (flagged by Greptile review): a custom-rendered or
# truncated message that ends mid-bracket with no `, input_type=` after
# the secret must still be scrubbed, not silently leaked.
text = "1 validation error\napi_token\n bad [type=str, input_value='sk-leak'"
scrubbed = sentry_mod._scrub_exception_value(text)
assert "sk-leak" not in scrubbed
assert "input_value=[Filtered]" in scrubbed
def test_before_breadcrumb_strips_query_string_for_http_categories() -> None:
crumb = {
"category": "httpx",
"data": {"url": "https://api.example.com/path?token=secret&id=42"},
}
sentry_mod._before_breadcrumb(crumb, {})
assert crumb["data"]["url"] == "https://api.example.com/path"
def test_before_breadcrumb_leaves_other_categories_alone() -> None:
crumb = {
"category": "console",
"data": {"url": "https://api.example.com/path?token=secret"},
}
sentry_mod._before_breadcrumb(crumb, {})
assert crumb["data"]["url"] == "https://api.example.com/path?token=secret"
def _clear_kill_switches(monkeypatch) -> None:
for env in (
"OPENSRE_SENTRY_DISABLED",
"OPENSRE_NO_TELEMETRY",
"DO_NOT_TRACK",
"OPENSRE_SENTRY_DSN",
"SENTRY_DSN",
):
monkeypatch.delenv(env, raising=False)
def _install_full_sentry_mock(monkeypatch):
init_mock = MagicMock()
tag_mock = MagicMock()
monkeypatch.setitem(
sys.modules,
"sentry_sdk",
SimpleNamespace(init=init_mock, set_tag=tag_mock),
)
return init_mock, tag_mock
def test_init_sentry_passes_explicit_integrations(monkeypatch) -> None:
from sentry_sdk.integrations.asyncio import AsyncioIntegration
from sentry_sdk.integrations.httpx import HttpxIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
real_integrations = [
LoggingIntegration(),
AsyncioIntegration(),
HttpxIntegration(),
]
monkeypatch.setattr(sentry_mod, "_build_sentry_integrations", lambda: real_integrations)
_clear_kill_switches(monkeypatch)
init_mock, _ = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
integrations = init_mock.call_args.kwargs["integrations"]
integration_names = {type(integration).__name__ for integration in integrations}
assert "LoggingIntegration" in integration_names
assert "AsyncioIntegration" in integration_names
assert "HttpxIntegration" in integration_names
def test_init_sentry_disables_auto_enabling_integrations(monkeypatch) -> None:
_clear_kill_switches(monkeypatch)
init_mock, _ = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
assert init_mock.call_args.kwargs["auto_enabling_integrations"] is False
def test_init_sentry_sets_in_app_include_app(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
init_mock, _ = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
assert init_mock.call_args.kwargs["in_app_include"] == ["app"]
def test_init_sentry_sets_max_breadcrumbs(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
init_mock, _ = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
assert init_mock.call_args.kwargs["max_breadcrumbs"] == 100
def test_init_sentry_sets_scope_tags(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
monkeypatch.setenv("OPENSRE_DEPLOYMENT_METHOD", "railway")
_, tag_mock = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="webapp")
tag_calls = {call.args for call in tag_mock.call_args_list}
assert ("entrypoint", "webapp") in tag_calls
assert ("opensre.runtime", "hosted") in tag_calls
assert ("deployment_method", "railway") in tag_calls
def test_init_sentry_entrypoint_defaults_to_unknown(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
_, tag_mock = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry()
tag_calls = {call.args for call in tag_mock.call_args_list}
assert ("entrypoint", "unknown") in tag_calls
def test_init_sentry_runtime_tag_is_cli_for_cli_entrypoint(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
_, tag_mock = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
tag_calls = {call.args for call in tag_mock.call_args_list}
assert ("opensre.runtime", "cli") in tag_calls
def test_init_sentry_runtime_tag_is_hosted_for_webapp_entrypoint(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
_, tag_mock = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="webapp")
tag_calls = {call.args for call in tag_mock.call_args_list}
assert ("opensre.runtime", "hosted") in tag_calls
def test_init_sentry_runtime_tag_is_cli_when_entrypoint_unknown(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
_, tag_mock = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry()
tag_calls = {call.args for call in tag_mock.call_args_list}
assert ("opensre.runtime", "cli") in tag_calls
def test_init_sentry_deployment_method_defaults_to_local(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
monkeypatch.delenv("OPENSRE_DEPLOYMENT_METHOD", raising=False)
_, tag_mock = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
tag_calls = {call.args for call in tag_mock.call_args_list}
assert ("deployment_method", "local") in tag_calls
def test_before_send_filters_request_body_recursively() -> None:
event = {
"request": {
"data": {
"system_prompt": "ignore previous instructions",
"nested": {"bearer": "ghp_xxx", "user_id": "ok"},
},
},
}
sentry_mod._before_send(event, {})
data = event["request"]["data"]
assert data["system_prompt"] == "[Filtered]"
assert data["nested"]["bearer"] == "[Filtered]"
assert data["nested"]["user_id"] == "ok"
def test_before_send_filters_request_body_with_substring_match() -> None:
event = {
"request": {
"body": {
"messages": [{"role": "user", "content": "hi"}],
"chat_messages_v2": "blob",
"app_dsn": "https://abc@sentry.invalid/1",
"request_id": "req-42",
},
},
}
sentry_mod._before_send(event, {})
body = event["request"]["body"]
assert body["messages"] == "[Filtered]"
assert body["chat_messages_v2"] == "[Filtered]"
assert body["app_dsn"] == "[Filtered]"
assert body["request_id"] == "req-42"
def test_before_send_filters_extra_keys_substring_match() -> None:
event = {
"extra": {
"prompt": "do bad things",
"chat_messages": "blob",
"bearer_session": "abc",
"user_credential_email": "x",
"user_email": "user@example.com",
"request_id": "req-42",
},
}
sentry_mod._before_send(event, {})
extra = event["extra"]
assert extra["prompt"] == "[Filtered]"
assert extra["chat_messages"] == "[Filtered]"
assert extra["bearer_session"] == "[Filtered]"
assert extra["user_credential_email"] == "[Filtered]"
assert extra["user_email"] == "user@example.com"
assert extra["request_id"] == "req-42"
def test_before_breadcrumb_filters_http_headers() -> None:
crumb = {
"category": "httpx",
"data": {
"headers": {
"Authorization": "Bearer secret",
"Cookie": "session=abc",
"User-Agent": "opensre/1",
},
},
}
sentry_mod._before_breadcrumb(crumb, {})
headers = crumb["data"]["headers"]
assert headers["Authorization"] == "[Filtered]"
assert headers["Cookie"] == "[Filtered]"
assert headers["User-Agent"] == "opensre/1"
def test_before_breadcrumb_filters_aiohttp_headers() -> None:
crumb = {
"category": "aiohttp",
"data": {"headers": {"authorization": "Bearer xyz", "X-Trace": "ok"}},
}
sentry_mod._before_breadcrumb(crumb, {})
headers = crumb["data"]["headers"]
assert headers["authorization"] == "[Filtered]"
assert headers["X-Trace"] == "ok"
def test_before_breadcrumb_does_not_touch_console_headers() -> None:
crumb = {
"category": "console",
"data": {"headers": {"Authorization": "Bearer secret"}},
}
sentry_mod._before_breadcrumb(crumb, {})
assert crumb["data"]["headers"]["Authorization"] == "Bearer secret"
def test_before_send_filters_nested_lists_of_dicts() -> None:
event = {
"request": {
"data": {
"batch": [[{"prompt": "leak"}, {"safe": "ok"}], [{"bearer": "x"}]],
},
},
}
sentry_mod._before_send(event, {})
nested = event["request"]["data"]["batch"]
assert nested[0][0]["prompt"] == "[Filtered]"
assert nested[0][1]["safe"] == "ok"
assert nested[1][0]["bearer"] == "[Filtered]"
@pytest.mark.parametrize(
("exc_type", "exc_value"),
[
(
"RuntimeError",
"Openai authentication failed. Check OPENAI_API_KEY in your environment, .env, or secure local keychain.",
),
(
"RuntimeError",
"Openrouter authentication failed. Check OPENROUTER_API_KEY in your environment, .env, or secure local keychain.",
),
(
"RuntimeError",
"Minimax authentication failed. Check MINIMAX_API_KEY in your environment, .env, or secure local keychain.",
),
(
"RuntimeError",
"1 validation error for LLMSettings\n Value error, LLM provider 'minimax' requires MINIMAX_API_KEY to be set.",
),
(
"RuntimeError",
"Openai request rejected (HTTP 400): Error code: 400 - {'error': {'message': 'litellm.BadRequestError: AnthropicException - {\"message\":\"The provided model identifier is invalid.\"}. Received Model Group=relay-ops-claude-opus-4-7'}}",
),
(
"RuntimeError",
"Openai rate limit exceeded (HTTP 429) after multiple retries. Check your quota and billing details.",
),
(
"BadRequestError",
"Your credit balance is too low to access the Anthropic API.",
),
(
"RuntimeError",
"Ollama model 'llama3.2' was not found. Check your configured model name or endpoint.",
),
(
"RuntimeError",
"LLM API request failed after multiple retries. Try again in a few seconds.",
),
(
"RuntimeError",
"Cannot connect to Ollama API. Check your network connection and that the endpoint URL is reachable.",
),
(
"RuntimeError",
"Cannot connect to Ollama API (SSL/TLS error). Verify the endpoint URL uses HTTPS and that no proxy is stripping TLS.",
),
(
"RuntimeError",
"Cannot connect to Openrouter API. Check your network connection and that the endpoint URL is reachable.",
),
# Provider read timeout after retries (issue #1934).
(
"RuntimeError",
"Openai API request timed out. Check that the service is running and responsive at the configured endpoint.",
),
(
"RuntimeError",
"Minimax API request timed out. Check that the service is running and responsive at the configured endpoint.",
),
# Anthropic account-level usage limit enforcement via HTTP 400 (issues #1883, #1885).
(
"RuntimeError",
"Anthropic request rejected (HTTP 400): Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'You have reached your specified API usage limits. You will regain access on 2026-06-01 at 00:00 UTC.'}, 'request_id': 'req_011CaxxMA8NCSdDvaM2LaRm6'}",
),
(
"RuntimeError",
"Bedrock model 'us.anthropic.claude-sonnet-4-6' is not available for your account. Check Bedrock model access in the configured AWS region, AWS Marketplace subscription/payment setup, and IAM permissions including aws-marketplace:ViewSubscriptions and aws-marketplace:Subscribe.",
),
# Bedrock cross-region inference profile misconfiguration (issue #2167).
(
"RuntimeError",
"Bedrock model 'anthropic.claude-haiku-4-5-20251001-v1:0' requires a cross-region inference profile. Try prefixing with 'us.' (e.g. 'us.anthropic.claude-haiku-4-5-20251001-v1:0') and update BEDROCK_REASONING_MODEL or BEDROCK_TOOLCALL_MODEL.",
),
# agent_llm_client uses "not found" (no "was") unlike llm_client — both must be caught.
(
"RuntimeError",
"Bedrock model 'anthropic.claude-3-sonnet-20240229-v1:0' not found.",
),
(
"RuntimeError",
"OpenAI model 'llama3.2' not found.",
),
],
)
def test_before_send_drops_operator_actionable_llm_errors(
exc_type: str,
exc_value: str,
) -> None:
event = {"exception": {"values": [{"type": exc_type, "value": exc_value}]}}
assert sentry_mod._before_send(event, {}) is None
def test_before_send_keeps_non_llm_runtime_errors() -> None:
event = {
"exception": {"values": [{"type": "RuntimeError", "value": "database invariant broke"}]}
}
assert sentry_mod._before_send(event, {}) == event
def test_init_sentry_skips_scope_tags_when_dsn_empty(monkeypatch) -> None:
sentry_mod._init_sentry_once.cache_clear()
_clear_kill_switches(monkeypatch)
monkeypatch.setenv("OPENSRE_SENTRY_DSN", "")
monkeypatch.setenv("SENTRY_DSN", "")
monkeypatch.setattr(sentry_mod, "SENTRY_DSN", "")
_, tag_mock = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
tag_mock.assert_not_called()
def test_before_send_filters_extra_recursively() -> None:
event = {
"extra": {
"context": {
"auth_token": "ghp_xxx",
"messages": [{"role": "user", "content": "hi"}],
"user_id": "ok",
},
"request_id": "req-42",
},
}
sentry_mod._before_send(event, {})
extra = event["extra"]
assert extra["context"]["auth_token"] == "[Filtered]"
assert extra["context"]["messages"] == "[Filtered]"
assert extra["context"]["user_id"] == "ok"
assert extra["request_id"] == "req-42"
def test_before_send_parses_json_string_request_body() -> None:
raw_body = (
'{"system_prompt": "you are an assistant",'
' "messages": [{"role": "user", "content": "hi"}],'
' "request_id": "req-1"}'
)
event = {"request": {"body": raw_body}}
sentry_mod._before_send(event, {})
body = event["request"]["body"]
assert isinstance(body, dict)
assert body["system_prompt"] == "[Filtered]"
assert body["messages"] == "[Filtered]"
assert body["request_id"] == "req-1"
def test_before_send_leaves_non_json_request_body_string_alone() -> None:
event = {"request": {"body": "not json"}}
sentry_mod._before_send(event, {})
assert event["request"]["body"] == "not json"
def test_init_sentry_does_not_double_init_across_entrypoints(monkeypatch) -> None:
_clear_kill_switches(monkeypatch)
init_mock, _ = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="webapp")
sentry_mod.init_sentry(entrypoint="pipeline")
init_mock.assert_called_once()
def test_apply_scope_tags_is_first_wins(monkeypatch) -> None:
_clear_kill_switches(monkeypatch)
_, tag_mock = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="webapp")
sentry_mod.init_sentry(entrypoint="pipeline")
entrypoint_tags = [
call.args[1] for call in tag_mock.call_args_list if call.args[0] == "entrypoint"
]
assert entrypoint_tags == ["webapp"]
def test_init_sentry_ignore_errors_includes_cli_transient_error(monkeypatch) -> None:
from integrations.llm_cli.errors import CLITransientError
_clear_kill_switches(monkeypatch)
init_mock, _ = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
ignore_errors = init_mock.call_args.kwargs["ignore_errors"]
assert CLITransientError in ignore_errors
def test_init_sentry_ignore_errors_includes_keyboard_interrupt(monkeypatch) -> None:
_clear_kill_switches(monkeypatch)
init_mock, _ = _install_full_sentry_mock(monkeypatch)
sentry_mod.init_sentry(entrypoint="cli")
ignore_errors = init_mock.call_args.kwargs["ignore_errors"]
assert KeyboardInterrupt in ignore_errors
def test_build_sentry_integrations_excludes_logging_when_disabled(monkeypatch) -> None:
monkeypatch.setenv("OPENSRE_SENTRY_LOGGING_DISABLED", "1")
integrations = _REAL_BUILD_INTEGRATIONS()
integration_names = {type(i).__name__ for i in integrations}
assert "LoggingIntegration" not in integration_names
assert "AsyncioIntegration" in integration_names
assert "HttpxIntegration" in integration_names
def test_build_sentry_integrations_includes_logging_by_default(monkeypatch) -> None:
monkeypatch.delenv("OPENSRE_SENTRY_LOGGING_DISABLED", raising=False)
integrations = _REAL_BUILD_INTEGRATIONS()
integration_names = {type(i).__name__ for i in integrations}
assert "LoggingIntegration" in integration_names
assert "AsyncioIntegration" in integration_names
assert "HttpxIntegration" in integration_names