diff --git a/src/blaxel/core/common/sentry.py b/src/blaxel/core/common/sentry.py index d253882..9c718ce 100644 --- a/src/blaxel/core/common/sentry.py +++ b/src/blaxel/core/common/sentry.py @@ -41,6 +41,40 @@ # Optional dependencies that may not be installed - import errors for these are expected _OPTIONAL_DEPENDENCIES = ("opentelemetry",) +# Optional blaxel framework integration subpackages. Importing any of these +# requires installing the matching extra (e.g. ``pip install blaxel[openai]``). +# When the extra -- or one of its transitive dependencies -- is missing, or when +# the integration files are absent from a stripped/partial install, importing the +# integration raises an ImportError. That is an expected environment issue, not an +# SDK bug, so it must not be reported to Sentry. +_OPTIONAL_INTEGRATION_PACKAGES = ( + "blaxel.langgraph", + "blaxel.llamaindex", + "blaxel.openai", + "blaxel.crewai", + "blaxel.googleadk", + "blaxel.livekit", + "blaxel.pydantic", + "blaxel.telemetry", +) + +_OPTIONAL_INTEGRATION_ENTRYPOINT_MODULES = { + "blaxel.langgraph": ("model", "tools"), + "blaxel.llamaindex": ("model", "tools"), + "blaxel.openai": ("model", "tools"), + "blaxel.crewai": ("model", "tools"), + "blaxel.googleadk": ("model", "tools"), + "blaxel.livekit": ("model", "tools"), + "blaxel.pydantic": ("model", "tools"), + "blaxel.telemetry": ("exporters", "instrumentation", "log", "manager", "span"), +} + +# Filesystem fragments used to detect an import error raised while loading one of +# the optional integration packages above (covers both POSIX and Windows paths). +_OPTIONAL_INTEGRATION_PATHS = tuple( + f"blaxel/{pkg.rsplit('.', 1)[-1]}/" for pkg in _OPTIONAL_INTEGRATION_PACKAGES +) + tuple(f"blaxel\\{pkg.rsplit('.', 1)[-1]}\\" for pkg in _OPTIONAL_INTEGRATION_PACKAGES) + # SDK path patterns to identify errors originating from our SDK _SDK_PATTERNS = [ "blaxel/", @@ -190,11 +224,80 @@ def _get_exception_key(exc_type, exc_value, frame) -> str: return f"{exc_name}:{exc_msg}:{origin}" -def _is_optional_dependency_error(exc_type, exc_value) -> bool: - """Check if the exception is an import error for an optional dependency.""" - if exc_type and issubclass(exc_type, ImportError): - msg = str(exc_value).lower() - return any(dep in msg for dep in _OPTIONAL_DEPENDENCIES) +def _has_optional_integration_frame(exc_value) -> bool: + """Check whether an exception traceback passed through an integration module.""" + tb = getattr(exc_value, "__traceback__", None) + while tb is not None: + filename = tb.tb_frame.f_code.co_filename + if any(path in filename for path in _OPTIONAL_INTEGRATION_PATHS): + return True + tb = tb.tb_next + return False + + +def _is_optional_integration_entrypoint_missing(missing: str) -> bool: + """Check whether the missing module is a public optional integration entrypoint.""" + if missing in _OPTIONAL_INTEGRATION_PACKAGES: + return True + + for package, entrypoints in _OPTIONAL_INTEGRATION_ENTRYPOINT_MODULES.items(): + if any(missing == f"{package}.{entrypoint}" for entrypoint in entrypoints): + return True + return False + + +def _is_optional_dependency_error(exc_type, exc_value, seen: set[int] | None = None) -> bool: + """Check if the exception is an import error that is expected when an optional + integration extra is not installed. + + These are environment issues (the user imported, e.g., ``blaxel.openai`` + without ``pip install blaxel[openai]``, or runs a stripped/partial install + that is missing the integration's modules) rather than SDK defects, so they + should not be reported to Sentry. + """ + if not (exc_type and issubclass(exc_type, ImportError)): + return False + + if seen is None: + seen = set() + exc_id = id(exc_value) + if exc_id in seen: + return False + seen.add(exc_id) + + # Name of the module that could not be imported, when available + # (e.g. "blaxel.openai.model", "agents", "opentelemetry.exporter.otlp"). + missing = getattr(exc_value, "name", None) or "" + + # 1) A public optional integration entrypoint itself is unavailable -- e.g. + # a stripped/partial install missing ``blaxel/openai/model.py``, + # surfacing as ModuleNotFoundError("No module named 'blaxel.openai.model'"). + # Do not suppress deeper ``blaxel..*`` misses: those may be + # real SDK packaging or internal import bugs and should still reach Sentry. + if _is_optional_integration_entrypoint_missing(missing): + return True + + # 2) A known optional third-party dependency could not be imported. + msg = str(exc_value).lower() + if any(dep in missing for dep in _OPTIONAL_DEPENDENCIES) or any( + dep in msg for dep in _OPTIONAL_DEPENDENCIES + ): + return True + + # 3) Optional integration import guards wrap the original import failure in + # a friendly ImportError with no module name. Suppress that wrapper when + # its explicit cause is already known optional-import noise. + cause = getattr(exc_value, "__cause__", None) + if isinstance(cause, ImportError) and _is_optional_dependency_error(type(cause), cause, seen): + return True + + # 4) A non-blaxel (third-party) import failed while loading an optional + # integration package -- i.e. the matching extra is not installed. Only + # treat non-blaxel modules this way so that genuine SDK import bugs (which + # fail on a "blaxel.*" module) are still captured. + if missing and not missing.startswith("blaxel") and _has_optional_integration_frame(exc_value): + return True + return False diff --git a/tests/core/test_sentry.py b/tests/core/test_sentry.py new file mode 100644 index 0000000..ca1a6ae --- /dev/null +++ b/tests/core/test_sentry.py @@ -0,0 +1,116 @@ +"""Tests for the lightweight Sentry error filter. + +The SDK installs a trace function (``sys.settrace``) that forwards exceptions +originating from ``site-packages/blaxel`` to Sentry. Import errors that happen +because an optional integration extra is not installed (or because a +stripped/partial install is missing the integration's modules) are environment +issues, not SDK defects, and must be filtered out before reaching Sentry. +""" + +from blaxel.core.common.sentry import ( + _OPTIONAL_INTEGRATION_ENTRYPOINT_MODULES, + _is_optional_dependency_error, +) + + +def _raise_in_file(filename: str, code: str) -> Exception: + """Execute ``code`` as if it lived in ``filename`` and return the exception.""" + try: + exec(compile(code, filename, "exec"), {}) + except Exception as e: # noqa: BLE001 - we want the raised exception object + return e + raise AssertionError("code did not raise") + + +class TestIsOptionalDependencyError: + """Cover the import-error classification used to suppress Sentry noise.""" + + def test_missing_integration_submodule_is_optional(self): + """The exact production symptom: a stripped install missing model.py. + + ``from .model import *`` in ``blaxel/openai/__init__.py`` raises + ``ModuleNotFoundError: No module named 'blaxel.openai.model'``. + """ + exc = ModuleNotFoundError( + "No module named 'blaxel.openai.model'", name="blaxel.openai.model" + ) + assert _is_optional_dependency_error(type(exc), exc) is True + + def test_missing_livekit_submodule_is_optional(self): + exc = ModuleNotFoundError( + "No module named 'blaxel.livekit.model'", name="blaxel.livekit.model" + ) + assert _is_optional_dependency_error(type(exc), exc) is True + + def test_each_optional_integration_package_is_covered(self): + for pkg in _OPTIONAL_INTEGRATION_ENTRYPOINT_MODULES: + exc = ModuleNotFoundError(f"No module named '{pkg}'", name=pkg) + assert _is_optional_dependency_error(type(exc), exc) is True, pkg + + def test_each_optional_integration_entrypoint_module_is_covered(self): + for pkg, entrypoints in _OPTIONAL_INTEGRATION_ENTRYPOINT_MODULES.items(): + for entrypoint in entrypoints: + missing = f"{pkg}.{entrypoint}" + exc = ModuleNotFoundError(f"No module named '{missing}'", name=missing) + assert _is_optional_dependency_error(type(exc), exc) is True, missing + + def test_opentelemetry_dependency_is_optional(self): + """Existing behavior: opentelemetry import errors are still suppressed.""" + exc = ModuleNotFoundError("No module named 'opentelemetry'", name="opentelemetry") + assert _is_optional_dependency_error(type(exc), exc) is True + + def test_missing_third_party_dep_while_loading_integration_is_optional(self): + """A missing extra dep (e.g. ``agents`` for blaxel[openai]) is expected.""" + exc = _raise_in_file( + "/usr/lib/python3.12/site-packages/blaxel/openai/model.py", + "raise ModuleNotFoundError(\"No module named 'agents'\", name='agents')", + ) + assert _is_optional_dependency_error(type(exc), exc) is True + + def test_missing_nested_blaxel_module_inside_integration_is_not_optional(self): + """Internal integration packaging/import bugs must still reach Sentry.""" + exc = ModuleNotFoundError( + "No module named 'blaxel.pydantic.custom.gemni'", + name="blaxel.pydantic.custom.gemni", + ) + assert _is_optional_dependency_error(ModuleNotFoundError, exc) is False + + def test_wrapped_integration_import_guard_error_is_optional(self): + """The friendly optional-extra guard from blaxel.openai stays quiet.""" + cause = ModuleNotFoundError( + "No module named 'blaxel.openai.model'", + name="blaxel.openai.model", + ) + exc = ImportError( + "The openai extra dependencies are required to use the OpenAI Agents integration. " + "Install them with: pip install blaxel[openai]" + ) + exc.__cause__ = cause + + assert _is_optional_dependency_error(type(exc), exc) is True + + def test_missing_third_party_dep_outside_integration_is_not_optional(self): + """A third-party import failure outside any optional integration (e.g. a + genuine missing core dependency) must still be reported.""" + exc = _raise_in_file( + "/usr/lib/python3.12/site-packages/blaxel/core/common/settings.py", + "raise ModuleNotFoundError(\"No module named 'httpx'\", name='httpx')", + ) + assert _is_optional_dependency_error(type(exc), exc) is False + + def test_core_module_import_error_is_not_optional(self): + """A genuine SDK bug failing on a ``blaxel.*`` core module is captured.""" + exc = ModuleNotFoundError( + "No module named 'blaxel.core.missing'", name="blaxel.core.missing" + ) + assert _is_optional_dependency_error(type(exc), exc) is False + + def test_non_import_error_is_not_optional(self): + exc = ValueError("not an import error") + assert _is_optional_dependency_error(type(exc), exc) is False + + def test_circular_import_error_cause_is_not_optional(self): + exc = ImportError("wrapped import failed") + exc.__cause__ = exc + + assert _is_optional_dependency_error(type(exc), exc) is False