1
+ from importlib import import_module
2
+
1
3
from sentry_sdk .hub import Hub , _should_send_default_pii
2
4
from sentry_sdk .integrations import DidNotEnable , Integration
3
5
from sentry_sdk .utils import parse_version
9
11
raise DidNotEnable ("graphql-core not installed" )
10
12
11
13
try :
12
- import ariadne
14
+ ariadne_graphql = import_module (
15
+ "ariadne.graphql"
16
+ ) # necessary because of some name shadowing shenanigans in ariadne
17
+ # XXX verify again
18
+ from ariadne .asgi import GraphQL as ASGIGraphQL
19
+ from ariadne .asgi .handlers import GraphQLHTTPHandler
20
+ from ariadne .asgi .handlers import http as ariadne_http
21
+ from ariadne .types import Extension
22
+ from ariadne .wsgi import GraphQL as WSGIGraphQL
13
23
except ImportError :
14
24
raise DidNotEnable ("ariadne not installed" )
15
25
16
26
17
27
if TYPE_CHECKING :
18
- from typing import Any
19
- from ariadne .graphql import GraphQLSchema
20
- from ariadne .types import GraphQLResult
28
+ from typing import Any , List
29
+ from ariadne .types import ContextValue , GraphQLError , GraphQLResult , GraphQLSchema
21
30
from sentry_sdk ._types import EventProcessor
22
31
23
32
24
33
class AriadneIntegration (Integration ):
25
34
identifier = "ariadne"
26
35
36
+ # XXX add an option to turn on error monitoring so that people who are instrumenting
37
+ # both the server and the client side can turn one of them off if they want
38
+ # XXX double patching
39
+
27
40
@staticmethod
28
41
def setup_once ():
29
42
# type: () -> None
30
43
# XXX version guard for ariadne
31
- # guard for double patching
32
44
version = parse_version (GRAPHQL_CORE_VERSION )
33
45
34
46
if version is None :
@@ -39,54 +51,156 @@ def setup_once():
39
51
if version < (3 , 2 ):
40
52
raise DidNotEnable ("graphql-core 3.2 or newer required." )
41
53
42
- old_graphql_sync = ariadne .graphql_sync
43
- old_graphql_async = ariadne .graphql
54
+ _patch_graphql ()
55
+ _inject_tracing_extension ()
56
+
57
+
58
+ def _patch_graphql ():
59
+ # type: () -> None
60
+ old_graphql_sync = ariadne_graphql .graphql_sync
61
+ old_graphql_async = ariadne_graphql .graphql
44
62
45
- def _sentry_patched_graphql_sync (schema , data , * args , ** kwargs ):
46
- # type: (GraphQLSchema, Any, Any, Any) -> GraphQLResult
47
- hub = Hub .current
48
- integration = hub .get_integration (AriadneIntegration )
49
- if integration is None or hub .client is None :
50
- return old_graphql_sync (schema , data , * args , ** kwargs )
63
+ def _sentry_patched_graphql_sync (schema , data , * args , ** kwargs ):
64
+ # type: (GraphQLSchema, Any, Any, Any) -> GraphQLResult
65
+ hub = Hub .current
66
+ integration = hub .get_integration (AriadneIntegration )
51
67
52
- result = old_graphql_sync (schema , data , * args , ** kwargs )
68
+ if integration is None or hub .client is None :
69
+ return old_graphql_sync (schema , data , * args , ** kwargs )
53
70
54
- _raise_errors (result , hub )
71
+ scope = hub .scope # XXX
72
+ if scope is not None :
73
+ event_processor = _make_event_processor (schema , data )
74
+ scope .add_event_processor (event_processor )
55
75
56
- return result
76
+ result = old_graphql_sync (schema , data , * args , ** kwargs )
77
+
78
+ _capture_graphql_errors (result , hub )
79
+
80
+ return result
57
81
58
- async def _sentry_patched_graphql_async (schema , data , * args , ** kwargs ):
59
- # type: (GraphQLSchema, Any, Any, Any) -> GraphQLResult
60
- hub = Hub .current
61
- integration = hub .get_integration (AriadneIntegration )
62
- if integration is None or hub .client is None :
63
- return await old_graphql_async (schema , data , * args , ** kwargs )
82
+ async def _sentry_patched_graphql_async (schema , data , * args , ** kwargs ):
83
+ # type: (GraphQLSchema, Any, Any, Any) -> GraphQLResult
84
+ hub = Hub .current
85
+ integration = hub .get_integration (AriadneIntegration )
64
86
65
- result = await old_graphql_async (schema , data , * args , ** kwargs )
87
+ if integration is None or hub .client is None :
88
+ return await old_graphql_async (schema , data , * args , ** kwargs )
66
89
67
- _raise_errors (result , hub )
90
+ scope = hub .scope # XXX
91
+ if scope is not None :
92
+ event_processor = _make_event_processor (schema , data )
93
+ scope .add_event_processor (event_processor )
68
94
69
- return result
95
+ result = await old_graphql_async (schema , data , * args , ** kwargs )
96
+ # XXX no result available in event processor like this
70
97
71
- ariadne .graphql_sync = _sentry_patched_graphql_sync
72
- ariadne .graphql_async = _sentry_patched_graphql_async
98
+ # XXX two options.
99
+ # a) Handle the exceptions in has_errors. Then it's actual exceptions that
100
+ # can be raised properly. But there is no response and it'd have to be
101
+ # constructed manually
102
+ # b) Handle the exceptions here once they've already been formatted. Then
103
+ # we need to reconstruct the exceptions instead before raising them.
104
+ # c?) Remember the exception somehow
105
+ # d) monkeypatch handle_graphql_errors
73
106
107
+ _capture_graphql_errors (result , hub )
74
108
75
- def _raise_errors (result , hub ):
76
- # type: (GraphQLResult, Hub) -> None
77
- data = result [1 ]
78
- if not isinstance (data , dict ) or not data .get ("errors" ):
79
109
return result
80
110
81
- for error in data ["errors" ]:
111
+ ariadne_graphql .graphql_sync = _sentry_patched_graphql_sync
112
+ ariadne_graphql .graphql = _sentry_patched_graphql_async
113
+ ariadne_http .graphql = _sentry_patched_graphql_async
114
+
115
+
116
+ def _inject_tracing_extension ():
117
+ # type: () -> None
118
+ old_asgi_http_handler_init = GraphQLHTTPHandler .__init__
119
+ old_asgi_init = ASGIGraphQL .__init__
120
+ old_wsgi_init = WSGIGraphQL .__init__
121
+
122
+ def _sentry_asgi_http_handler_wrapper (self , * args , ** kwargs ):
123
+ # type: (GraphQLHTTPHandler, Any, Any) -> None
124
+ extensions = kwargs .get ("extensions" ) or []
125
+ extensions .append (SentryTracingExtension )
126
+ kwargs ["extensions" ] = extensions
127
+ old_asgi_http_handler_init (self , * args , ** kwargs )
128
+
129
+ def _sentry_asgi_wrapper (self , * args , ** kwargs ):
130
+ # type: (ASGIGraphQL, Any, Any) -> None
131
+ http_handler = kwargs .get ("http_handler" )
132
+ if http_handler is None :
133
+ http_handler = GraphQLHTTPHandler ()
134
+ kwargs ["http_handler" ] = http_handler
135
+ old_asgi_init (self , * args , ** kwargs )
136
+
137
+ def _sentry_wsgi_wrapper (self , * args , ** kwargs ):
138
+ # type: (WSGIGraphQL, Any, Any) -> None
139
+ extensions = kwargs .get ("extensions" ) or []
140
+ extensions .append (SentryTracingExtension )
141
+ kwargs ["extensions" ] = extensions
142
+ old_wsgi_init (self , * args , ** kwargs )
143
+
144
+ GraphQLHTTPHandler .__init__ = _sentry_asgi_http_handler_wrapper
145
+ ASGIGraphQL .__init__ = _sentry_asgi_wrapper
146
+ WSGIGraphQL .__init__ = _sentry_wsgi_wrapper
147
+
148
+
149
+ class SentryTracingExtension (Extension ):
150
+ def __init__ (self ):
151
+ pass
152
+
153
+ def request_started (self , context ):
154
+ # type: (ContextValue) -> None
155
+ print ("req started ctx" , context )
156
+ hub = Hub .current
157
+ integration = hub .get_integration (AriadneIntegration )
158
+ if integration is None :
159
+ return
160
+
161
+ def request_finished (self , context ):
162
+ # type: (ContextValue) -> None
163
+ print ("req finished ctx" , context )
164
+ hub = Hub .current
165
+ integration = hub .get_integration (AriadneIntegration )
166
+ if integration is None or hub .client is None :
167
+ return
168
+
169
+ def has_errors (self , errors , context ):
170
+ # type: (List[GraphQLError], ContextValue) -> None
171
+ # XXX could use this to capture exceptions but we don't have access
172
+ # to the response here
173
+ print ("has errors ctx" , context )
174
+ hub = Hub .current
175
+ integration = hub .get_integration (AriadneIntegration )
176
+ if integration is None or hub .client is None :
177
+ return
178
+
179
+ # XXX def resolve()
180
+
181
+
182
+ def _capture_graphql_errors (result , hub ):
183
+ result = result [1 ]
184
+ if not isinstance (result , dict ):
185
+ return
186
+
187
+ for error in result .get ("errors" ) or []:
188
+ # have to reconstruct the exceptions here since they've already been
189
+ # formatted
82
190
hub .capture_exception (error )
83
191
84
192
85
- def _make_event_processor ():
86
- # type: () -> EventProcessor
193
+ def _make_event_processor (schema , data ):
194
+ # type: (GraphQLSchema, Any ) -> EventProcessor
87
195
def inner (event , hint ):
196
+ print ("in event processor" )
197
+ print (schema )
198
+ print (data )
88
199
if _should_send_default_pii ():
89
- pass
200
+ if isinstance (data , dict ) and data .get ("query" ):
201
+ request_info = event .setdefault ("request" , {})
202
+ request_info ["api_target" ] = "graphql"
203
+ request_info ["data" ] = str (data ["query" ])
90
204
91
205
return event
92
206
0 commit comments