diff --git a/sentry/hooks.py b/sentry/hooks.py index 6a6af920967..eb1ee9bc6c8 100644 --- a/sentry/hooks.py +++ b/sentry/hooks.py @@ -19,6 +19,7 @@ _logger = logging.getLogger(__name__) HAS_SENTRY_SDK = True +_ORIGINAL_APPLICATION_CALL = None try: import sentry_sdk from sentry_sdk.integrations.logging import ignore_logger @@ -136,12 +137,29 @@ def initialize_sentry(config): for item in exclude_loggers: ignore_logger(item) - # The server app is already registered so patch it here - if server: + global _ORIGINAL_APPLICATION_CALL + + # The server app is already registered so patch it here. + # this is mostly a defensive fallback + if server and server.app is not odoo.http.root: server.app = SentryWsgiMiddleware(server.app) - # Patch the wsgi server in case of further registration - odoo.http.Application = SentryWsgiMiddleware(odoo.http.Application) + # Patch the actual WSGI entrypoint while keeping + # odoo.http.Application as a class + # odoo.http.root as the regular root/application object + if _ORIGINAL_APPLICATION_CALL is None: + _ORIGINAL_APPLICATION_CALL = odoo.http.Application.__call__ + + def sentry_application_call(self, environ, start_response): + middleware = getattr(self, "_sentry_wsgi_middleware", None) + if middleware is None: + middleware = SentryWsgiMiddleware( + _ORIGINAL_APPLICATION_CALL.__get__(self, type(self)) + ) + self._sentry_wsgi_middleware = middleware + return middleware(environ, start_response) + + odoo.http.Application.__call__ = sentry_application_call with sentry_sdk.new_scope() as scope: scope.set_extra("debug", False) diff --git a/sentry/tests/test_client.py b/sentry/tests/test_client.py index e506582caad..e25d50cac7b 100644 --- a/sentry/tests/test_client.py +++ b/sentry/tests/test_client.py @@ -1,13 +1,16 @@ # Copyright 2016-2017 Versada +# Copyright 2026 Therp BV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - +import inspect import logging import sys from unittest.mock import patch from sentry_sdk.integrations.logging import _IGNORED_LOGGERS +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.transport import HttpTransport +import odoo.http from odoo import exceptions from odoo.tests import TransactionCase from odoo.tools import config @@ -266,3 +269,71 @@ def test_config_release(self, get_odoo_commit): RELEASE, "Failed to use 'sentry_release' parameter appropriately", ) + + def test_initialize_sentry_patches_application_call_when_server_missing(self): + original_root = odoo.http.root + original_application = odoo.http.Application + + class DummyRoot: + def __init__(self): + self.session_store = object() + + class DummyApplication: + def __call__(self, environ, start_response): + return "ok" + + try: + dummy_root = DummyRoot() + original_call = DummyApplication.__call__ + odoo.http.root = dummy_root + odoo.http.Application = DummyApplication + with ( + patch("odoo.addons.sentry.hooks.server", new=None), + patch("odoo.addons.sentry.hooks._ORIGINAL_APPLICATION_CALL", new=None), + ): + initialize_sentry(config) + self.assertTrue(inspect.isclass(odoo.http.Application)) + self.assertIs(odoo.http.root, dummy_root) + self.assertTrue(hasattr(odoo.http.root, "session_store")) + self.assertIs(odoo.http.root.session_store, dummy_root.session_store) + self.assertIsNot(odoo.http.Application.__call__, original_call) + finally: + odoo.http.root = original_root + odoo.http.Application = original_application + + def test_initialize_sentry_wraps_server_app_and_patches_application_call(self): + original_root = odoo.http.root + original_application = odoo.http.Application + + class DummyRoot: + def __init__(self): + self.session_store = object() + + class DummyApplication: + def __call__(self, environ, start_response): + return "ok" + + class DummyServer: + def __init__(self): + self.app = lambda environ, start_response: "server-ok" + + dummy_server = DummyServer() + try: + dummy_root = DummyRoot() + original_call = DummyApplication.__call__ + odoo.http.root = dummy_root + odoo.http.Application = DummyApplication + with ( + patch("odoo.addons.sentry.hooks.server", new=dummy_server), + patch("odoo.addons.sentry.hooks._ORIGINAL_APPLICATION_CALL", new=None), + ): + initialize_sentry(config) + self.assertIsInstance(dummy_server.app, SentryWsgiMiddleware) + self.assertTrue(inspect.isclass(odoo.http.Application)) + self.assertIs(odoo.http.root, dummy_root) + self.assertTrue(hasattr(odoo.http.root, "session_store")) + self.assertIs(odoo.http.root.session_store, dummy_root.session_store) + self.assertIsNot(odoo.http.Application.__call__, original_call) + finally: + odoo.http.root = original_root + odoo.http.Application = original_application