Skip to content

Commit b357fd5

Browse files
authored
Add Graphene GraphQL error integration (#2389)
Capture GraphQL errors when using Graphene and add more context to them (request data with syntax highlighting, if applicable).
1 parent 2faf03d commit b357fd5

File tree

4 files changed

+416
-0
lines changed

4 files changed

+416
-0
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Test graphene
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- release/**
8+
9+
pull_request:
10+
11+
# Cancel in progress workflows on pull_requests.
12+
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
15+
cancel-in-progress: true
16+
17+
permissions:
18+
contents: read
19+
20+
env:
21+
BUILD_CACHE_KEY: ${{ github.sha }}
22+
CACHED_BUILD_PATHS: |
23+
${{ github.workspace }}/dist-serverless
24+
25+
jobs:
26+
test:
27+
name: graphene, python ${{ matrix.python-version }}, ${{ matrix.os }}
28+
runs-on: ${{ matrix.os }}
29+
timeout-minutes: 30
30+
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
python-version: ["3.7","3.8","3.9","3.10","3.11"]
35+
# python3.6 reached EOL and is no longer being supported on
36+
# new versions of hosted runners on Github Actions
37+
# ubuntu-20.04 is the last version that supported python3.6
38+
# see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877
39+
os: [ubuntu-20.04]
40+
41+
steps:
42+
- uses: actions/checkout@v4
43+
- uses: actions/setup-python@v4
44+
with:
45+
python-version: ${{ matrix.python-version }}
46+
47+
- name: Setup Test Env
48+
run: |
49+
pip install coverage "tox>=3,<4"
50+
51+
- name: Test graphene
52+
uses: nick-fields/retry@v2
53+
with:
54+
timeout_minutes: 15
55+
max_attempts: 2
56+
retry_wait_seconds: 5
57+
shell: bash
58+
command: |
59+
set -x # print commands that are executed
60+
coverage erase
61+
62+
# Run tests
63+
./scripts/runtox.sh "py${{ matrix.python-version }}-graphene" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch &&
64+
coverage combine .coverage* &&
65+
coverage xml -i
66+
67+
- uses: codecov/codecov-action@v3
68+
with:
69+
token: ${{ secrets.CODECOV_TOKEN }}
70+
files: coverage.xml
71+
72+
73+
check_required_tests:
74+
name: All graphene tests passed or skipped
75+
needs: test
76+
# Always run this, even if a dependent job failed
77+
if: always()
78+
runs-on: ubuntu-20.04
79+
steps:
80+
- name: Check for failures
81+
if: contains(needs.test.result, 'failure')
82+
run: |
83+
echo "One of the dependent jobs has failed. You may need to re-run it." && exit 1

sentry_sdk/integrations/graphene.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from sentry_sdk.hub import Hub, _should_send_default_pii
2+
from sentry_sdk.integrations import DidNotEnable, Integration
3+
from sentry_sdk.integrations.modules import _get_installed_modules
4+
from sentry_sdk.utils import (
5+
capture_internal_exceptions,
6+
event_from_exception,
7+
parse_version,
8+
)
9+
from sentry_sdk._types import TYPE_CHECKING
10+
11+
12+
try:
13+
from graphene.types import schema as graphene_schema # type: ignore
14+
except ImportError:
15+
raise DidNotEnable("graphene is not installed")
16+
17+
18+
if TYPE_CHECKING:
19+
from typing import Any, Dict, Union
20+
from graphene.language.source import Source # type: ignore
21+
from graphql.execution import ExecutionResult # type: ignore
22+
from graphql.type import GraphQLSchema # type: ignore
23+
24+
25+
class GrapheneIntegration(Integration):
26+
identifier = "graphene"
27+
28+
@staticmethod
29+
def setup_once():
30+
# type: () -> None
31+
installed_packages = _get_installed_modules()
32+
version = parse_version(installed_packages["graphene"])
33+
34+
if version is None:
35+
raise DidNotEnable("Unparsable graphene version: {}".format(version))
36+
37+
if version < (3, 3):
38+
raise DidNotEnable("graphene 3.3 or newer required.")
39+
40+
_patch_graphql()
41+
42+
43+
def _patch_graphql():
44+
# type: () -> None
45+
old_graphql_sync = graphene_schema.graphql_sync
46+
old_graphql_async = graphene_schema.graphql
47+
48+
def _sentry_patched_graphql_sync(schema, source, *args, **kwargs):
49+
# type: (GraphQLSchema, Union[str, Source], Any, Any) -> ExecutionResult
50+
hub = Hub.current
51+
integration = hub.get_integration(GrapheneIntegration)
52+
if integration is None:
53+
return old_graphql_sync(schema, source, *args, **kwargs)
54+
55+
with hub.configure_scope() as scope:
56+
scope.add_event_processor(_event_processor)
57+
58+
result = old_graphql_sync(schema, source, *args, **kwargs)
59+
60+
with capture_internal_exceptions():
61+
for error in result.errors or []:
62+
event, hint = event_from_exception(
63+
error,
64+
client_options=hub.client.options if hub.client else None,
65+
mechanism={
66+
"type": integration.identifier,
67+
"handled": False,
68+
},
69+
)
70+
hub.capture_event(event, hint=hint)
71+
72+
return result
73+
74+
async def _sentry_patched_graphql_async(schema, source, *args, **kwargs):
75+
# type: (GraphQLSchema, Union[str, Source], Any, Any) -> ExecutionResult
76+
hub = Hub.current
77+
integration = hub.get_integration(GrapheneIntegration)
78+
if integration is None:
79+
return await old_graphql_async(schema, source, *args, **kwargs)
80+
81+
with hub.configure_scope() as scope:
82+
scope.add_event_processor(_event_processor)
83+
84+
result = await old_graphql_async(schema, source, *args, **kwargs)
85+
86+
with capture_internal_exceptions():
87+
for error in result.errors or []:
88+
event, hint = event_from_exception(
89+
error,
90+
client_options=hub.client.options if hub.client else None,
91+
mechanism={
92+
"type": integration.identifier,
93+
"handled": False,
94+
},
95+
)
96+
hub.capture_event(event, hint=hint)
97+
98+
return result
99+
100+
graphene_schema.graphql_sync = _sentry_patched_graphql_sync
101+
graphene_schema.graphql = _sentry_patched_graphql_async
102+
103+
104+
def _event_processor(event, hint):
105+
# type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
106+
if _should_send_default_pii():
107+
request_info = event.setdefault("request", {})
108+
request_info["api_target"] = "graphql"
109+
110+
elif event.get("request", {}).get("data"):
111+
del event["request"]["data"]
112+
113+
return event

0 commit comments

Comments
 (0)