gql.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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() -> None:
  35. gql_version = parse_version(gql.__version__)
  36. _check_minimum_version(GQLIntegration, gql_version)
  37. _patch_execute()
  38. def _data_from_document(document: "DocumentNode") -> "EventDataType":
  39. try:
  40. operation_ast = get_operation_ast(document)
  41. data: "EventDataType" = {"query": print_ast(document)}
  42. if operation_ast is not None:
  43. data["variables"] = operation_ast.variable_definitions
  44. if operation_ast.name is not None:
  45. data["operationName"] = operation_ast.name.value
  46. return data
  47. except (AttributeError, TypeError):
  48. return dict()
  49. def _transport_method(transport: "Union[Transport, AsyncTransport]") -> str:
  50. """
  51. The RequestsHTTPTransport allows defining the HTTP method; all
  52. other transports use POST.
  53. """
  54. try:
  55. return transport.method
  56. except AttributeError:
  57. return "POST"
  58. def _request_info_from_transport(
  59. transport: "Union[Transport, AsyncTransport, None]",
  60. ) -> "Dict[str, str]":
  61. if transport is None:
  62. return {}
  63. request_info = {
  64. "method": _transport_method(transport),
  65. }
  66. try:
  67. request_info["url"] = transport.url
  68. except AttributeError:
  69. pass
  70. return request_info
  71. def _patch_execute() -> None:
  72. real_execute = gql.Client.execute
  73. # Maintain signature for backwards compatibility.
  74. # gql.Client.execute() accepts a positional-only "request"
  75. # parameter with version 4.0.0.
  76. @ensure_integration_enabled(GQLIntegration, real_execute)
  77. def sentry_patched_execute(
  78. self: "gql.Client",
  79. document: "DocumentNode",
  80. *args: "Any",
  81. **kwargs: "Any",
  82. ) -> "Any":
  83. scope = sentry_sdk.get_isolation_scope()
  84. # document is a gql.GraphQLRequest with gql v4.0.0.
  85. scope.add_event_processor(_make_gql_event_processor(self, document))
  86. try:
  87. # document is a gql.GraphQLRequest with gql v4.0.0.
  88. return real_execute(self, document, *args, **kwargs)
  89. except TransportQueryError as e:
  90. event, hint = event_from_exception(
  91. e,
  92. client_options=sentry_sdk.get_client().options,
  93. mechanism={"type": "gql", "handled": False},
  94. )
  95. sentry_sdk.capture_event(event, hint)
  96. raise e
  97. gql.Client.execute = sentry_patched_execute
  98. def _make_gql_event_processor(
  99. client: "gql.Client", document_or_request: "Union[DocumentNode, gql.GraphQLRequest]"
  100. ) -> "EventProcessor":
  101. def processor(event: "Event", hint: "dict[str, Any]") -> "Event":
  102. try:
  103. errors = hint["exc_info"][1].errors
  104. except (AttributeError, KeyError):
  105. errors = None
  106. request = event.setdefault("request", {})
  107. request.update(
  108. {
  109. "api_target": "graphql",
  110. **_request_info_from_transport(client.transport),
  111. }
  112. )
  113. if should_send_default_pii():
  114. if GraphQLRequest is not None and isinstance(
  115. document_or_request, GraphQLRequest
  116. ):
  117. # In v4.0.0, gql moved to using GraphQLRequest instead of
  118. # DocumentNode in execute
  119. # https://github.com/graphql-python/gql/pull/556
  120. document = document_or_request.document
  121. else:
  122. document = document_or_request
  123. request["data"] = _data_from_document(document)
  124. contexts = event.setdefault("contexts", {})
  125. response = contexts.setdefault("response", {})
  126. response.update(
  127. {
  128. "data": {"errors": errors},
  129. "type": response,
  130. }
  131. )
  132. return event
  133. return processor