forked from getsentry/sentry-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add GraphQL client integration (getsentry#2368)
* Monkeypatch * Sending actual errors now * Fix mypy typing * Add GQL requirements to Tox * Add Tox dependencies * Fix mypy * More meaningful patched function name * some basic unit tests * Created GQL Tox env * Updated YAML for CI * Added importorskip for gql tests * More unit tests * Improved mocking for unit tests * Explain each test * added two integration tests for good measure * Skip loading gql tests in python below 3.7 * Fix module name * Actually should have fixed module name now * Install optional gql dependencies in tox * Fix error in Py 3.7 * Ignore capitalized variable * Added doc comment to pytest_ignore_collect * Check successful gql import * Switch to type comments * Made test loadable in Python 2 * Added version check * Make sure integration is there before doing sentry stuff * Removed breakpoint * Using EventProcessor * Fix typing * Change to version comment Co-authored-by: Ivana Kellyerova <ivana.kellyerova@sentry.io> * Address code review * TYPE_CHECKING from sentry_sdk._types Co-authored-by: Ivana Kellyerova <ivana.kellyerova@sentry.io> --------- Co-authored-by: Ivana Kellyerova <ivana.kellyerova@sentry.io>
- Loading branch information
1 parent
641822d
commit 6908aad
Showing
4 changed files
with
450 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
name: Test gql | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
- release/** | ||
|
||
pull_request: | ||
|
||
# Cancel in progress workflows on pull_requests. | ||
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value | ||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||
cancel-in-progress: true | ||
|
||
permissions: | ||
contents: read | ||
|
||
env: | ||
BUILD_CACHE_KEY: ${{ github.sha }} | ||
CACHED_BUILD_PATHS: | | ||
${{ github.workspace }}/dist-serverless | ||
jobs: | ||
test: | ||
name: gql, python ${{ matrix.python-version }}, ${{ matrix.os }} | ||
runs-on: ${{ matrix.os }} | ||
timeout-minutes: 30 | ||
|
||
strategy: | ||
fail-fast: false | ||
matrix: | ||
python-version: ["3.7","3.8","3.9","3.10","3.11"] | ||
# python3.6 reached EOL and is no longer being supported on | ||
# new versions of hosted runners on Github Actions | ||
# ubuntu-20.04 is the last version that supported python3.6 | ||
# see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 | ||
os: [ubuntu-20.04] | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: actions/setup-python@v4 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
|
||
- name: Setup Test Env | ||
run: | | ||
pip install coverage "tox>=3,<4" | ||
- name: Test gql | ||
uses: nick-fields/retry@v2 | ||
with: | ||
timeout_minutes: 15 | ||
max_attempts: 2 | ||
retry_wait_seconds: 5 | ||
shell: bash | ||
command: | | ||
set -x # print commands that are executed | ||
coverage erase | ||
# Run tests | ||
./scripts/runtox.sh "py${{ matrix.python-version }}-gql" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch && | ||
coverage combine .coverage* && | ||
coverage xml -i | ||
- uses: codecov/codecov-action@v3 | ||
with: | ||
token: ${{ secrets.CODECOV_TOKEN }} | ||
files: coverage.xml | ||
|
||
|
||
check_required_tests: | ||
name: All gql tests passed or skipped | ||
needs: test | ||
# Always run this, even if a dependent job failed | ||
if: always() | ||
runs-on: ubuntu-20.04 | ||
steps: | ||
- name: Check for failures | ||
if: contains(needs.test.result, 'failure') | ||
run: | | ||
echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
from sentry_sdk.utils import event_from_exception, parse_version | ||
from sentry_sdk.hub import Hub, _should_send_default_pii | ||
from sentry_sdk.integrations import DidNotEnable, Integration | ||
|
||
try: | ||
import gql # type: ignore[import] | ||
from graphql import print_ast, get_operation_ast, DocumentNode, VariableDefinitionNode # type: ignore[import] | ||
from gql.transport import Transport, AsyncTransport # type: ignore[import] | ||
from gql.transport.exceptions import TransportQueryError # type: ignore[import] | ||
except ImportError: | ||
raise DidNotEnable("gql is not installed") | ||
|
||
from sentry_sdk._types import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from typing import Any, Dict, Tuple, Union | ||
from sentry_sdk._types import EventProcessor | ||
|
||
EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] | ||
|
||
MIN_GQL_VERSION = (3, 4, 1) | ||
|
||
|
||
class GQLIntegration(Integration): | ||
identifier = "gql" | ||
|
||
@staticmethod | ||
def setup_once(): | ||
# type: () -> None | ||
gql_version = parse_version(gql.__version__) | ||
if gql_version is None or gql_version < MIN_GQL_VERSION: | ||
raise DidNotEnable( | ||
"GQLIntegration is only supported for GQL versions %s and above." | ||
% ".".join(str(num) for num in MIN_GQL_VERSION) | ||
) | ||
_patch_execute() | ||
|
||
|
||
def _data_from_document(document): | ||
# type: (DocumentNode) -> EventDataType | ||
try: | ||
operation_ast = get_operation_ast(document) | ||
data = {"query": print_ast(document)} # type: EventDataType | ||
|
||
if operation_ast is not None: | ||
data["variables"] = operation_ast.variable_definitions | ||
if operation_ast.name is not None: | ||
data["operationName"] = operation_ast.name.value | ||
|
||
return data | ||
except (AttributeError, TypeError): | ||
return dict() | ||
|
||
|
||
def _transport_method(transport): | ||
# type: (Union[Transport, AsyncTransport]) -> str | ||
""" | ||
The RequestsHTTPTransport allows defining the HTTP method; all | ||
other transports use POST. | ||
""" | ||
try: | ||
return transport.method | ||
except AttributeError: | ||
return "POST" | ||
|
||
|
||
def _request_info_from_transport(transport): | ||
# type: (Union[Transport, AsyncTransport, None]) -> Dict[str, str] | ||
if transport is None: | ||
return {} | ||
|
||
request_info = { | ||
"method": _transport_method(transport), | ||
} | ||
|
||
try: | ||
request_info["url"] = transport.url | ||
except AttributeError: | ||
pass | ||
|
||
return request_info | ||
|
||
|
||
def _patch_execute(): | ||
# type: () -> None | ||
real_execute = gql.Client.execute | ||
|
||
def sentry_patched_execute(self, document, *args, **kwargs): | ||
# type: (gql.Client, DocumentNode, Any, Any) -> Any | ||
hub = Hub.current | ||
if hub.get_integration(GQLIntegration) is None: | ||
return real_execute(self, document, *args, **kwargs) | ||
|
||
with Hub.current.configure_scope() as scope: | ||
scope.add_event_processor(_make_gql_event_processor(self, document)) | ||
|
||
try: | ||
return real_execute(self, document, *args, **kwargs) | ||
except TransportQueryError as e: | ||
event, hint = event_from_exception( | ||
e, | ||
client_options=hub.client.options if hub.client is not None else None, | ||
mechanism={"type": "gql", "handled": False}, | ||
) | ||
|
||
hub.capture_event(event, hint) | ||
raise e | ||
|
||
gql.Client.execute = sentry_patched_execute | ||
|
||
|
||
def _make_gql_event_processor(client, document): | ||
# type: (gql.Client, DocumentNode) -> EventProcessor | ||
def processor(event, hint): | ||
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] | ||
try: | ||
errors = hint["exc_info"][1].errors | ||
except (AttributeError, KeyError): | ||
errors = None | ||
|
||
request = event.setdefault("request", {}) | ||
request.update( | ||
{ | ||
"api_target": "graphql", | ||
**_request_info_from_transport(client.transport), | ||
} | ||
) | ||
|
||
if _should_send_default_pii(): | ||
request["data"] = _data_from_document(document) | ||
contexts = event.setdefault("contexts", {}) | ||
response = contexts.setdefault("response", {}) | ||
response.update( | ||
{ | ||
"data": {"errors": errors}, | ||
"type": response, | ||
} | ||
) | ||
|
||
return event | ||
|
||
return processor |
Oops, something went wrong.