|
| 1 | +import json |
1 | 2 | import sys
|
2 | 3 | import weakref
|
3 | 4 |
|
| 5 | +try: |
| 6 | + from urllib.parse import parse_qsl |
| 7 | +except ImportError: |
| 8 | + from urlparse import parse_qsl # type: ignore |
| 9 | + |
4 | 10 | from sentry_sdk.api import continue_trace
|
5 | 11 | from sentry_sdk._compat import reraise
|
6 | 12 | from sentry_sdk.consts import OP, SPANDATA
|
7 |
| -from sentry_sdk.hub import Hub |
| 13 | +from sentry_sdk.hub import Hub, _should_send_default_pii |
8 | 14 | from sentry_sdk.integrations import Integration, DidNotEnable
|
9 | 15 | from sentry_sdk.integrations.logging import ignore_logger
|
10 | 16 | from sentry_sdk.sessions import auto_session_tracking
|
|
29 | 35 | CONTEXTVARS_ERROR_MESSAGE,
|
30 | 36 | SENSITIVE_DATA_SUBSTITUTE,
|
31 | 37 | AnnotatedValue,
|
| 38 | + SentryGraphQLClientError, |
| 39 | + _get_graphql_operation_name, |
| 40 | + _get_graphql_operation_type, |
32 | 41 | )
|
33 | 42 |
|
34 | 43 | try:
|
35 | 44 | import asyncio
|
36 | 45 |
|
37 | 46 | from aiohttp import __version__ as AIOHTTP_VERSION
|
38 |
| - from aiohttp import ClientSession, TraceConfig |
39 |
| - from aiohttp.web import Application, HTTPException, UrlDispatcher |
| 47 | + from aiohttp import ClientSession, ContentTypeError, TraceConfig |
| 48 | + from aiohttp.web import Application, HTTPException, UrlDispatcher, Response |
40 | 49 | except ImportError:
|
41 | 50 | raise DidNotEnable("AIOHTTP not installed")
|
42 | 51 |
|
|
45 | 54 | if TYPE_CHECKING:
|
46 | 55 | from aiohttp.web_request import Request
|
47 | 56 | from aiohttp.abc import AbstractMatchInfo
|
48 |
| - from aiohttp import TraceRequestStartParams, TraceRequestEndParams |
| 57 | + from aiohttp import ( |
| 58 | + TraceRequestStartParams, |
| 59 | + TraceRequestEndParams, |
| 60 | + TraceRequestChunkSentParams, |
| 61 | + ) |
49 | 62 | from types import SimpleNamespace
|
50 | 63 | from typing import Any
|
51 | 64 | from typing import Dict
|
|
64 | 77 | class AioHttpIntegration(Integration):
|
65 | 78 | identifier = "aiohttp"
|
66 | 79 |
|
67 |
| - def __init__(self, transaction_style="handler_name"): |
68 |
| - # type: (str) -> None |
| 80 | + def __init__(self, transaction_style="handler_name", capture_graphql_errors=True): |
| 81 | + # type: (str, bool) -> None |
69 | 82 | if transaction_style not in TRANSACTION_STYLE_VALUES:
|
70 | 83 | raise ValueError(
|
71 | 84 | "Invalid value for transaction_style: %s (must be in %s)"
|
72 | 85 | % (transaction_style, TRANSACTION_STYLE_VALUES)
|
73 | 86 | )
|
74 | 87 | self.transaction_style = transaction_style
|
75 | 88 |
|
| 89 | + self.capture_graphql_errors = capture_graphql_errors |
| 90 | + |
76 | 91 | @staticmethod
|
77 | 92 | def setup_once():
|
78 | 93 | # type: () -> None
|
@@ -111,7 +126,7 @@ async def sentry_app_handle(self, request, *args, **kwargs):
|
111 | 126 | # create a task to wrap each request.
|
112 | 127 | with hub.configure_scope() as scope:
|
113 | 128 | scope.clear_breadcrumbs()
|
114 |
| - scope.add_event_processor(_make_request_processor(weak_request)) |
| 129 | + scope.add_event_processor(_make_server_processor(weak_request)) |
115 | 130 |
|
116 | 131 | transaction = continue_trace(
|
117 | 132 | request.headers,
|
@@ -139,6 +154,7 @@ async def sentry_app_handle(self, request, *args, **kwargs):
|
139 | 154 | reraise(*_capture_exception(hub))
|
140 | 155 |
|
141 | 156 | transaction.set_http_status(response.status)
|
| 157 | + |
142 | 158 | return response
|
143 | 159 |
|
144 | 160 | Application._handle = sentry_app_handle
|
@@ -198,7 +214,8 @@ def create_trace_config():
|
198 | 214 | async def on_request_start(session, trace_config_ctx, params):
|
199 | 215 | # type: (ClientSession, SimpleNamespace, TraceRequestStartParams) -> None
|
200 | 216 | hub = Hub.current
|
201 |
| - if hub.get_integration(AioHttpIntegration) is None: |
| 217 | + integration = hub.get_integration(AioHttpIntegration) |
| 218 | + if integration is None: |
202 | 219 | return
|
203 | 220 |
|
204 | 221 | method = params.method.upper()
|
@@ -233,28 +250,95 @@ async def on_request_start(session, trace_config_ctx, params):
|
233 | 250 | params.headers[key] = value
|
234 | 251 |
|
235 | 252 | trace_config_ctx.span = span
|
| 253 | + trace_config_ctx.is_graphql_request = params.url.path == "/graphql" |
| 254 | + |
| 255 | + if integration.capture_graphql_errors and trace_config_ctx.is_graphql_request: |
| 256 | + trace_config_ctx.request_headers = params.headers |
| 257 | + |
| 258 | + async def on_request_chunk_sent(session, trace_config_ctx, params): |
| 259 | + # type: (ClientSession, SimpleNamespace, TraceRequestChunkSentParams) -> None |
| 260 | + integration = Hub.current.get_integration(AioHttpIntegration) |
| 261 | + if integration is None: |
| 262 | + return |
| 263 | + |
| 264 | + if integration.capture_graphql_errors and trace_config_ctx.is_graphql_request: |
| 265 | + trace_config_ctx.request_body = None |
| 266 | + with capture_internal_exceptions(): |
| 267 | + try: |
| 268 | + trace_config_ctx.request_body = json.loads(params.chunk) |
| 269 | + except json.JSONDecodeError: |
| 270 | + return |
236 | 271 |
|
237 | 272 | async def on_request_end(session, trace_config_ctx, params):
|
238 | 273 | # type: (ClientSession, SimpleNamespace, TraceRequestEndParams) -> None
|
239 |
| - if trace_config_ctx.span is None: |
| 274 | + hub = Hub.current |
| 275 | + integration = hub.get_integration(AioHttpIntegration) |
| 276 | + if integration is None: |
240 | 277 | return
|
241 | 278 |
|
242 |
| - span = trace_config_ctx.span |
243 |
| - span.set_http_status(int(params.response.status)) |
244 |
| - span.set_data("reason", params.response.reason) |
245 |
| - span.finish() |
| 279 | + response = params.response |
| 280 | + |
| 281 | + if trace_config_ctx.span is not None: |
| 282 | + span = trace_config_ctx.span |
| 283 | + span.set_http_status(int(response.status)) |
| 284 | + span.set_data("reason", response.reason) |
| 285 | + |
| 286 | + if ( |
| 287 | + integration.capture_graphql_errors |
| 288 | + and trace_config_ctx.is_graphql_request |
| 289 | + and response.method in ("GET", "POST") |
| 290 | + and response.status == 200 |
| 291 | + ): |
| 292 | + with hub.configure_scope() as scope: |
| 293 | + with capture_internal_exceptions(): |
| 294 | + try: |
| 295 | + response_content = await response.json() |
| 296 | + except ContentTypeError: |
| 297 | + pass |
| 298 | + else: |
| 299 | + scope.add_event_processor( |
| 300 | + _make_client_processor( |
| 301 | + trace_config_ctx=trace_config_ctx, |
| 302 | + response=response, |
| 303 | + response_content=response_content, |
| 304 | + ) |
| 305 | + ) |
| 306 | + |
| 307 | + if ( |
| 308 | + response_content |
| 309 | + and isinstance(response_content, dict) |
| 310 | + and response_content.get("errors") |
| 311 | + ): |
| 312 | + try: |
| 313 | + raise SentryGraphQLClientError |
| 314 | + except SentryGraphQLClientError as ex: |
| 315 | + event, hint = event_from_exception( |
| 316 | + ex, |
| 317 | + client_options=hub.client.options |
| 318 | + if hub.client |
| 319 | + else None, |
| 320 | + mechanism={ |
| 321 | + "type": AioHttpIntegration.identifier, |
| 322 | + "handled": False, |
| 323 | + }, |
| 324 | + ) |
| 325 | + hub.capture_event(event, hint=hint) |
| 326 | + |
| 327 | + if trace_config_ctx.span is not None: |
| 328 | + span.finish() |
246 | 329 |
|
247 | 330 | trace_config = TraceConfig()
|
248 | 331 |
|
249 | 332 | trace_config.on_request_start.append(on_request_start)
|
| 333 | + trace_config.on_request_chunk_sent.append(on_request_chunk_sent) |
250 | 334 | trace_config.on_request_end.append(on_request_end)
|
251 | 335 |
|
252 | 336 | return trace_config
|
253 | 337 |
|
254 | 338 |
|
255 |
| -def _make_request_processor(weak_request): |
| 339 | +def _make_server_processor(weak_request): |
256 | 340 | # type: (Callable[[], Request]) -> EventProcessor
|
257 |
| - def aiohttp_processor( |
| 341 | + def aiohttp_server_processor( |
258 | 342 | event, # type: Dict[str, Any]
|
259 | 343 | hint, # type: Dict[str, Tuple[type, BaseException, Any]]
|
260 | 344 | ):
|
@@ -286,7 +370,63 @@ def aiohttp_processor(
|
286 | 370 |
|
287 | 371 | return event
|
288 | 372 |
|
289 |
| - return aiohttp_processor |
| 373 | + return aiohttp_server_processor |
| 374 | + |
| 375 | + |
| 376 | +def _make_client_processor(trace_config_ctx, response, response_content): |
| 377 | + # type: (SimpleNamespace, Response, Optional[Dict[str, Any]]) -> EventProcessor |
| 378 | + def aiohttp_client_processor( |
| 379 | + event, # type: Dict[str, Any] |
| 380 | + hint, # type: Dict[str, Tuple[type, BaseException, Any]] |
| 381 | + ): |
| 382 | + # type: (...) -> Dict[str, Any] |
| 383 | + with capture_internal_exceptions(): |
| 384 | + request_info = event.setdefault("request", {}) |
| 385 | + |
| 386 | + parsed_url = parse_url(str(response.url), sanitize=False) |
| 387 | + request_info["url"] = parsed_url.url |
| 388 | + request_info["method"] = response.method |
| 389 | + |
| 390 | + if getattr(trace_config_ctx, "request_headers", None): |
| 391 | + request_info["headers"] = _filter_headers( |
| 392 | + dict(trace_config_ctx.request_headers) |
| 393 | + ) |
| 394 | + |
| 395 | + if _should_send_default_pii(): |
| 396 | + if getattr(trace_config_ctx, "request_body", None): |
| 397 | + request_info["data"] = trace_config_ctx.request_body |
| 398 | + |
| 399 | + request_info["query_string"] = parsed_url.query |
| 400 | + |
| 401 | + if response.url.path == "/graphql": |
| 402 | + request_info["api_target"] = "graphql" |
| 403 | + |
| 404 | + query = request_info.get("data") |
| 405 | + if response.method == "GET": |
| 406 | + query = dict(parse_qsl(parsed_url.query)) |
| 407 | + |
| 408 | + if query: |
| 409 | + operation_name = _get_graphql_operation_name(query) |
| 410 | + operation_type = _get_graphql_operation_type(query) |
| 411 | + event["fingerprint"] = [ |
| 412 | + operation_name, |
| 413 | + operation_type, |
| 414 | + response.status, |
| 415 | + ] |
| 416 | + event["exception"]["values"][0][ |
| 417 | + "value" |
| 418 | + ] = "GraphQL request failed, name: {}, type: {}".format( |
| 419 | + operation_name, operation_type |
| 420 | + ) |
| 421 | + |
| 422 | + if _should_send_default_pii() and response_content: |
| 423 | + contexts = event.setdefault("contexts", {}) |
| 424 | + response_context = contexts.setdefault("response", {}) |
| 425 | + response_context["data"] = response_content |
| 426 | + |
| 427 | + return event |
| 428 | + |
| 429 | + return aiohttp_client_processor |
290 | 430 |
|
291 | 431 |
|
292 | 432 | def _capture_exception(hub):
|
|
0 commit comments