Skip to content

Commit 18015e9

Browse files
czyberantonpirkerszokeasaurusrex
authored
feat(graphene): Add span for grapqhl operation (#2788)
This commit adds a span for a GraphQL operation to the graphene integration. Fixes #2765 --------- Co-authored-by: Anton Pirker <[email protected]> Co-authored-by: Daniel Szoke <[email protected]>
1 parent 088589a commit 18015e9

File tree

2 files changed

+134
-5
lines changed

2 files changed

+134
-5
lines changed

sentry_sdk/integrations/graphene.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from contextlib import contextmanager
2+
13
import sentry_sdk
4+
from sentry_sdk.consts import OP
25
from sentry_sdk.integrations import DidNotEnable, Integration
36
from sentry_sdk.scope import Scope, should_send_default_pii
47
from sentry_sdk.utils import (
@@ -17,6 +20,7 @@
1720

1821

1922
if TYPE_CHECKING:
23+
from collections.abc import Generator
2024
from typing import Any, Dict, Union
2125
from graphene.language.source import Source # type: ignore
2226
from graphql.execution import ExecutionResult # type: ignore
@@ -52,13 +56,15 @@ def _sentry_patched_graphql_sync(schema, source, *args, **kwargs):
5256
scope = Scope.get_isolation_scope()
5357
scope.add_event_processor(_event_processor)
5458

55-
result = old_graphql_sync(schema, source, *args, **kwargs)
59+
with graphql_span(schema, source, kwargs):
60+
result = old_graphql_sync(schema, source, *args, **kwargs)
5661

5762
with capture_internal_exceptions():
63+
client = sentry_sdk.get_client()
5864
for error in result.errors or []:
5965
event, hint = event_from_exception(
6066
error,
61-
client_options=sentry_sdk.get_client().options,
67+
client_options=client.options,
6268
mechanism={
6369
"type": GrapheneIntegration.identifier,
6470
"handled": False,
@@ -70,19 +76,22 @@ def _sentry_patched_graphql_sync(schema, source, *args, **kwargs):
7076

7177
async def _sentry_patched_graphql_async(schema, source, *args, **kwargs):
7278
# type: (GraphQLSchema, Union[str, Source], Any, Any) -> ExecutionResult
73-
if sentry_sdk.get_client().get_integration(GrapheneIntegration) is None:
79+
integration = sentry_sdk.get_client().get_integration(GrapheneIntegration)
80+
if integration is None:
7481
return await old_graphql_async(schema, source, *args, **kwargs)
7582

7683
scope = Scope.get_isolation_scope()
7784
scope.add_event_processor(_event_processor)
7885

79-
result = await old_graphql_async(schema, source, *args, **kwargs)
86+
with graphql_span(schema, source, kwargs):
87+
result = await old_graphql_async(schema, source, *args, **kwargs)
8088

8189
with capture_internal_exceptions():
90+
client = sentry_sdk.get_client()
8291
for error in result.errors or []:
8392
event, hint = event_from_exception(
8493
error,
85-
client_options=sentry_sdk.get_client().options,
94+
client_options=client.options,
8695
mechanism={
8796
"type": GrapheneIntegration.identifier,
8897
"handled": False,
@@ -106,3 +115,43 @@ def _event_processor(event, hint):
106115
del event["request"]["data"]
107116

108117
return event
118+
119+
120+
@contextmanager
121+
def graphql_span(schema, source, kwargs):
122+
# type: (GraphQLSchema, Union[str, Source], Dict[str, Any]) -> Generator[None, None, None]
123+
operation_name = kwargs.get("operation_name")
124+
125+
operation_type = "query"
126+
op = OP.GRAPHQL_QUERY
127+
if source.strip().startswith("mutation"):
128+
operation_type = "mutation"
129+
op = OP.GRAPHQL_MUTATION
130+
elif source.strip().startswith("subscription"):
131+
operation_type = "subscription"
132+
op = OP.GRAPHQL_SUBSCRIPTION
133+
134+
sentry_sdk.add_breadcrumb(
135+
crumb={
136+
"data": {
137+
"operation_name": operation_name,
138+
"operation_type": operation_type,
139+
},
140+
"category": "graphql.operation",
141+
},
142+
)
143+
144+
scope = Scope.get_current_scope()
145+
if scope.span:
146+
_graphql_span = scope.span.start_child(op=op, description=operation_name)
147+
else:
148+
_graphql_span = sentry_sdk.start_span(op=op, description=operation_name)
149+
150+
_graphql_span.set_data("graphql.document", source)
151+
_graphql_span.set_data("graphql.operation.name", operation_name)
152+
_graphql_span.set_data("graphql.operation.type", operation_type)
153+
154+
try:
155+
yield
156+
finally:
157+
_graphql_span.finish()

tests/integrations/graphene/test_graphene.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from flask import Flask, request, jsonify
44
from graphene import ObjectType, String, Schema
55

6+
from sentry_sdk.consts import OP
67
from sentry_sdk.integrations.fastapi import FastApiIntegration
78
from sentry_sdk.integrations.flask import FlaskIntegration
89
from sentry_sdk.integrations.graphene import GrapheneIntegration
@@ -201,3 +202,82 @@ def graphql_server_sync():
201202
client.post("/graphql", json=query)
202203

203204
assert len(events) == 0
205+
206+
207+
def test_graphql_span_holds_query_information(sentry_init, capture_events):
208+
sentry_init(
209+
integrations=[GrapheneIntegration(), FlaskIntegration()],
210+
enable_tracing=True,
211+
default_integrations=False,
212+
)
213+
events = capture_events()
214+
215+
schema = Schema(query=Query)
216+
217+
sync_app = Flask(__name__)
218+
219+
@sync_app.route("/graphql", methods=["POST"])
220+
def graphql_server_sync():
221+
data = request.get_json()
222+
result = schema.execute(data["query"], operation_name=data.get("operationName"))
223+
return jsonify(result.data), 200
224+
225+
query = {
226+
"query": "query GreetingQuery { hello }",
227+
"operationName": "GreetingQuery",
228+
}
229+
client = sync_app.test_client()
230+
client.post("/graphql", json=query)
231+
232+
assert len(events) == 1
233+
234+
(event,) = events
235+
assert len(event["spans"]) == 1
236+
237+
(span,) = event["spans"]
238+
assert span["op"] == OP.GRAPHQL_QUERY
239+
assert span["description"] == query["operationName"]
240+
assert span["data"]["graphql.document"] == query["query"]
241+
assert span["data"]["graphql.operation.name"] == query["operationName"]
242+
assert span["data"]["graphql.operation.type"] == "query"
243+
244+
245+
def test_breadcrumbs_hold_query_information_on_error(sentry_init, capture_events):
246+
sentry_init(
247+
integrations=[
248+
GrapheneIntegration(),
249+
],
250+
default_integrations=False,
251+
)
252+
events = capture_events()
253+
254+
schema = Schema(query=Query)
255+
256+
sync_app = Flask(__name__)
257+
258+
@sync_app.route("/graphql", methods=["POST"])
259+
def graphql_server_sync():
260+
data = request.get_json()
261+
result = schema.execute(data["query"], operation_name=data.get("operationName"))
262+
return jsonify(result.data), 200
263+
264+
query = {
265+
"query": "query ErrorQuery { goodbye }",
266+
"operationName": "ErrorQuery",
267+
}
268+
client = sync_app.test_client()
269+
client.post("/graphql", json=query)
270+
271+
assert len(events) == 1
272+
273+
(event,) = events
274+
assert len(event["breadcrumbs"]) == 1
275+
276+
breadcrumbs = event["breadcrumbs"]["values"]
277+
assert len(breadcrumbs) == 1
278+
279+
(breadcrumb,) = breadcrumbs
280+
assert breadcrumb["category"] == "graphql.operation"
281+
assert breadcrumb["data"]["operation_name"] == query["operationName"]
282+
assert breadcrumb["data"]["operation_type"] == "query"
283+
assert breadcrumb["type"] == "default"

0 commit comments

Comments
 (0)