gql.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import sentry_sdk
  2. from sentry_sdk.utils import (
  3. event_from_exception,
  4. ensure_integration_enabled,
  5. parse_version,
  6. )
  7. from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
  8. from sentry_sdk.scope import should_send_default_pii
  9. try:
  10. import gql # type: ignore[import-not-found]
  11. from graphql import (
  12. print_ast,
  13. get_operation_ast,
  14. DocumentNode,
  15. VariableDefinitionNode,
  16. )
  17. from gql.transport import Transport, AsyncTransport # type: ignore[import-not-found]
  18. from gql.transport.exceptions import TransportQueryError # type: ignore[import-not-found]
  19. try:
  20. # gql 4.0+
  21. from gql import GraphQLRequest
  22. except ImportError:
  23. GraphQLRequest = None
  24. except ImportError:
  25. raise DidNotEnable("gql is not installed")
  26. from typing import TYPE_CHECKING
  27. if TYPE_CHECKING:
  28. from typing import Any, Dict, Tuple, Union
  29. from sentry_sdk._types import Event, EventProcessor
  30. EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]]
  31. class GQLIntegration(Integration):
  32. identifier = "gql"
  33. @staticmethod
  34. def setup_once():
  35. # type: () -> None
  36. gql_version = parse_version(gql.__version__)
  37. _check_minimum_version(GQLIntegration, gql_version)
  38. _patch_execute()
  39. def _data_from_document(document):
  40. # type: (DocumentNode) -> EventDataType
  41. try:
  42. operation_ast = get_operation_ast(document)
  43. data = {"query": print_ast(document)} # type: EventDataType
  44. if operation_ast is not None:
  45. data["variables"] = operation_ast.variable_definitions
  46. if operation_ast.name is not None:
  47. data["operationName"] = operation_ast.name.value
  48. return data
  49. except (AttributeError, TypeError):
  50. return dict()
  51. def _transport_method(transport):
  52. # type: (Union[Transport, AsyncTransport]) -> str
  53. """
  54. The RequestsHTTPTransport allows defining the HTTP method; all
  55. other transports use POST.
  56. """
  57. try:
  58. return transport.method
  59. except AttributeError:
  60. return "POST"
  61. def _request_info_from_transport(transport):
  62. # type: (Union[Transport, AsyncTransport, None]) -> Dict[str, str]
  63. if transport is None:
  64. return {}
  65. request_info = {
  66. "method": _transport_method(transport),
  67. }
  68. try:
  69. request_info["url"] = transport.url
  70. except AttributeError:
  71. pass
  72. return request_info
  73. def _patch_execute():
  74. # type: () -> None
  75. real_execute = gql.Client.execute
  76. @ensure_integration_enabled(GQLIntegration, real_execute)
  77. def sentry_patched_execute(self, document_or_request, *args, **kwargs):
  78. # type: (gql.Client, DocumentNode, Any, Any) -> Any
  79. scope = sentry_sdk.get_isolation_scope()
  80. scope.add_event_processor(_make_gql_event_processor(self, document_or_request))
  81. try:
  82. return real_execute(self, document_or_request, *args, **kwargs)
  83. except TransportQueryError as e:
  84. event, hint = event_from_exception(
  85. e,
  86. client_options=sentry_sdk.get_client().options,
  87. mechanism={"type": "gql", "handled": False},
  88. )
  89. sentry_sdk.capture_event(event, hint)
  90. raise e
  91. gql.Client.execute = sentry_patched_execute
  92. def _make_gql_event_processor(client, document_or_request):
  93. # type: (gql.Client, Union[DocumentNode, gql.GraphQLRequest]) -> EventProcessor
  94. def processor(event, hint):
  95. # type: (Event, dict[str, Any]) -> Event
  96. try:
  97. errors = hint["exc_info"][1].errors
  98. except (AttributeError, KeyError):
  99. errors = None
  100. request = event.setdefault("request", {})
  101. request.update(
  102. {
  103. "api_target": "graphql",
  104. **_request_info_from_transport(client.transport),
  105. }
  106. )
  107. if should_send_default_pii():
  108. if GraphQLRequest is not None and isinstance(
  109. document_or_request, GraphQLRequest
  110. ):
  111. # In v4.0.0, gql moved to using GraphQLRequest instead of
  112. # DocumentNode in execute
  113. # https://github.com/graphql-python/gql/pull/556
  114. document = document_or_request.document
  115. else:
  116. document = document_or_request
  117. request["data"] = _data_from_document(document)
  118. contexts = event.setdefault("contexts", {})
  119. response = contexts.setdefault("response", {})
  120. response.update(
  121. {
  122. "data": {"errors": errors},
  123. "type": response,
  124. }
  125. )
  126. return event
  127. return processor