Skip to content

Commit 1b8644b

Browse files
n1nguuntitaker
authored andcommitted
Trytond integration (getsentry#548)
Sentry SDK integration for the Trytond ERP framework
1 parent 0c93613 commit 1b8644b

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

sentry_sdk/integrations/trytond.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import sentry_sdk.hub
2+
import sentry_sdk.utils
3+
import sentry_sdk.integrations
4+
import sentry_sdk.integrations.wsgi
5+
from sentry_sdk._types import MYPY
6+
7+
from trytond.exceptions import TrytonException # type: ignore
8+
from trytond.wsgi import app # type: ignore
9+
10+
if MYPY:
11+
from typing import Any
12+
13+
14+
# TODO: trytond-worker, trytond-cron and trytond-admin intergations
15+
16+
17+
class TrytondWSGIIntegration(sentry_sdk.integrations.Integration):
18+
identifier = "trytond_wsgi"
19+
20+
def __init__(self): # type: () -> None
21+
pass
22+
23+
@staticmethod
24+
def setup_once(): # type: () -> None
25+
26+
app.wsgi_app = sentry_sdk.integrations.wsgi.SentryWsgiMiddleware(app.wsgi_app)
27+
28+
def error_handler(e): # type: (Exception) -> None
29+
hub = sentry_sdk.hub.Hub.current
30+
31+
if hub.get_integration(TrytondWSGIIntegration) is None:
32+
return
33+
elif isinstance(e, TrytonException):
34+
return
35+
else:
36+
# If an integration is there, a client has to be there.
37+
client = hub.client # type: Any
38+
event, hint = sentry_sdk.utils.event_from_exception(
39+
e,
40+
client_options=client.options,
41+
mechanism={"type": "trytond", "handled": False},
42+
)
43+
hub.capture_event(event, hint=hint)
44+
45+
# Expected error handlers signature was changed
46+
# when the error_handler decorator was introduced
47+
# in Tryton-5.4
48+
if hasattr(app, "error_handler"):
49+
50+
@app.error_handler
51+
def _(app, request, e): # type: ignore
52+
error_handler(e)
53+
54+
else:
55+
app.error_handlers.append(error_handler)
+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import pytest
2+
3+
pytest.importorskip("trytond")
4+
5+
import json
6+
import unittest.mock
7+
8+
import trytond
9+
from trytond.exceptions import TrytonException as TrytondBaseException
10+
from trytond.exceptions import UserError as TrytondUserError
11+
from trytond.exceptions import UserWarning as TrytondUserWarning
12+
from trytond.exceptions import LoginException
13+
from trytond.wsgi import app as trytond_app
14+
15+
from werkzeug.test import Client
16+
from sentry_sdk import last_event_id
17+
from sentry_sdk.integrations.trytond import TrytondWSGIIntegration
18+
19+
20+
@pytest.fixture(scope="function")
21+
def app(sentry_init):
22+
yield trytond_app
23+
24+
25+
@pytest.fixture
26+
def get_client(app):
27+
def inner():
28+
return Client(app)
29+
30+
return inner
31+
32+
33+
@pytest.mark.parametrize(
34+
"exception", [Exception("foo"), type("FooException", (Exception,), {})("bar")]
35+
)
36+
def test_exceptions_captured(
37+
sentry_init, app, capture_exceptions, get_client, exception
38+
):
39+
sentry_init(integrations=[TrytondWSGIIntegration()])
40+
exceptions = capture_exceptions()
41+
42+
unittest.mock.sentinel.exception = exception
43+
44+
@app.route("/exception")
45+
def _(request):
46+
raise unittest.mock.sentinel.exception
47+
48+
client = get_client()
49+
_ = client.get("/exception")
50+
51+
(e,) = exceptions
52+
assert e is exception
53+
54+
55+
@pytest.mark.parametrize(
56+
"exception",
57+
[
58+
TrytondUserError("title"),
59+
TrytondUserWarning("title", "details"),
60+
LoginException("title", "details"),
61+
],
62+
)
63+
def test_trytonderrors_not_captured(
64+
sentry_init, app, capture_exceptions, get_client, exception
65+
):
66+
sentry_init(integrations=[TrytondWSGIIntegration()])
67+
exceptions = capture_exceptions()
68+
69+
unittest.mock.sentinel.exception = exception
70+
71+
@app.route("/usererror")
72+
def _(request):
73+
raise unittest.mock.sentinel.exception
74+
75+
client = get_client()
76+
_ = client.get("/usererror")
77+
78+
assert not exceptions
79+
80+
81+
@pytest.mark.skipif(
82+
trytond.__version__.split(".") < ["5", "4"], reason="At least Trytond-5.4 required"
83+
)
84+
def test_rpc_error_page(sentry_init, app, capture_events, get_client):
85+
"""Test that, after initializing the Trytond-SentrySDK integration
86+
a custom error handler can be registered to the Trytond WSGI app so as to
87+
inform the event identifiers to the Tryton RPC client"""
88+
89+
sentry_init(integrations=[TrytondWSGIIntegration()])
90+
events = capture_events()
91+
92+
@app.route("/rpcerror", methods=["POST"])
93+
def _(request):
94+
raise Exception("foo")
95+
96+
@app.error_handler
97+
def _(app, request, e):
98+
if isinstance(e, TrytondBaseException):
99+
return
100+
else:
101+
event_id = last_event_id()
102+
data = TrytondUserError(str(event_id), str(e))
103+
return app.make_response(request, data)
104+
105+
client = get_client()
106+
107+
# This would look like a natural Tryton RPC call
108+
_data = dict(
109+
id=42, # request sequence
110+
method="class.method", # rpc call
111+
params=[
112+
[1234], # ids
113+
["bar", "baz"], # values
114+
dict( # context
115+
client="12345678-9abc-def0-1234-56789abc",
116+
groups=[1],
117+
language="ca",
118+
language_direction="ltr",
119+
),
120+
],
121+
)
122+
response = client.post(
123+
"/rpcerror", content_type="application/json", data=json.dumps(_data)
124+
)
125+
126+
(event,) = events
127+
(content, status, headers) = response
128+
data = json.loads(next(content))
129+
assert status == "200 OK"
130+
assert headers.get("Content-Type") == "application/json"
131+
assert data == dict(id=42, error=["UserError", [event["event_id"], "foo", None]])

tox.ini

+11
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ envlist =
4949

5050
{py3.7,py3.8}-tornado-{5,6}
5151

52+
{py3.4}-trytond-{4.6,4.8,5.0}
53+
{py3.5}-trytond-{4.6,4.8,5.0,5.2}
54+
{py3.6,py3.7,py3.8}-trytond-{4.6,4.8,5.0,5.2,5.4}
55+
5256
{py2.7,py3.8}-requests
5357

5458
{py2.7,py3.7,py3.8}-redis
@@ -148,6 +152,12 @@ deps =
148152
tornado-5: tornado>=5,<6
149153
tornado-6: tornado>=6.0a1
150154

155+
trytond-5.4: trytond>=5.4,<5.5
156+
trytond-5.2: trytond>=5.2,<5.3
157+
trytond-5.0: trytond>=5.0,<5.1
158+
trytond-4.8: trytond>=4.8,<4.9
159+
trytond-4.6: trytond>=4.6,<4.7
160+
151161
redis: fakeredis
152162
# https://github.com/jamesls/fakeredis/issues/245
153163
redis: redis<3.2.2
@@ -184,6 +194,7 @@ setenv =
184194
rq: TESTPATH=tests/integrations/rq
185195
aiohttp: TESTPATH=tests/integrations/aiohttp
186196
tornado: TESTPATH=tests/integrations/tornado
197+
trytond: TESTPATH=tests/integrations/trytond
187198
redis: TESTPATH=tests/integrations/redis
188199
asgi: TESTPATH=tests/integrations/asgi
189200
sqlalchemy: TESTPATH=tests/integrations/sqlalchemy

0 commit comments

Comments
 (0)