Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(integrations): Add span for grapqhl operation #2765 #2788

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 47 additions & 8 deletions sentry_sdk/integrations/graphene.py
@@ -1,3 +1,7 @@
from contextlib import contextmanager

from sentry_sdk import start_span
from sentry_sdk.consts import OP
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.utils import (
Expand Down Expand Up @@ -50,10 +54,8 @@ def _sentry_patched_graphql_sync(schema, source, *args, **kwargs):
if integration is None:
return old_graphql_sync(schema, source, *args, **kwargs)

with hub.configure_scope() as scope:
scope.add_event_processor(_event_processor)

result = old_graphql_sync(schema, source, *args, **kwargs)
with graphql_span(hub, schema, source, kwargs):
result = old_graphql_sync(schema, source, *args, **kwargs)

with capture_internal_exceptions():
for error in result.errors or []:
Expand All @@ -76,10 +78,8 @@ async def _sentry_patched_graphql_async(schema, source, *args, **kwargs):
if integration is None:
return await old_graphql_async(schema, source, *args, **kwargs)

with hub.configure_scope() as scope:
scope.add_event_processor(_event_processor)

result = await old_graphql_async(schema, source, *args, **kwargs)
with graphql_span(hub, schema, source, kwargs):
result = await old_graphql_async(schema, source, *args, **kwargs)

with capture_internal_exceptions():
for error in result.errors or []:
Expand Down Expand Up @@ -109,3 +109,42 @@ def _event_processor(event, hint):
del event["request"]["data"]

return event


@contextmanager
def graphql_span(hub, schema, source, kwargs):
operation_name = kwargs.get("operation_name")

operation_type = "query"
op = OP.GRAPHQL_QUERY
if source.strip().startswith("mutation"):
operation_type = "mutation"
op = OP.GRAPHQL_MUTATION
elif source.strip().startswith("subscription"):
operation_type = "subscription"
op = OP.GRAPHQL_SUBSCRIPTION

hub.add_breadcrumb(
crumb={
"data": {
"operation_name": operation_name,
"operation_type": operation_type,
},
"category": "graphql.operation",
},
)

with hub.configure_scope() as scope:
if scope.span:
_graphql_span = scope.span.start_child(op=op, description=operation_name)
else:
_graphql_span = start_span(op=op, description=operation_name)
scope.add_event_processor(_event_processor)

_graphql_span.set_data("graphql.document", source)
_graphql_span.set_data("graphql.operation.name", operation_name)
_graphql_span.set_data("graphql.operation.type", operation_type)

yield

_graphql_span.finish()
80 changes: 80 additions & 0 deletions tests/integrations/graphene/test_graphene_py3.py
Expand Up @@ -3,6 +3,7 @@
from flask import Flask, request, jsonify
from graphene import ObjectType, String, Schema

from sentry_sdk.consts import OP
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.graphene import GrapheneIntegration
Expand Down Expand Up @@ -201,3 +202,82 @@ def graphql_server_sync():
client.post("/graphql", json=query)

assert len(events) == 0


def test_graphql_span_holds_query_information(sentry_init, capture_events):
sentry_init(
integrations=[GrapheneIntegration(), FlaskIntegration()],
enable_tracing=True,
default_integrations=False,
)
events = capture_events()

schema = Schema(query=Query)

sync_app = Flask(__name__)

@sync_app.route("/graphql", methods=["POST"])
def graphql_server_sync():
data = request.get_json()
result = schema.execute(data["query"], operation_name=data.get("operationName"))
return jsonify(result.data), 200

query = {
"query": "query GreetingQuery { hello }",
"operationName": "GreetingQuery",
}
client = sync_app.test_client()
client.post("/graphql", json=query)

assert len(events) == 1

(event,) = events
assert len(event["spans"]) == 1

(span,) = event["spans"]
assert span["op"] == OP.GRAPHQL_QUERY
assert span["description"] == query["operationName"]
assert span["data"]["graphql.document"] == query["query"]
assert span["data"]["graphql.operation.name"] == query["operationName"]
assert span["data"]["graphql.operation.type"] == "query"


def test_breadcrumbs_hold_query_information_on_error(sentry_init, capture_events):
sentry_init(
integrations=[
GrapheneIntegration(),
],
default_integrations=False,
)
events = capture_events()

schema = Schema(query=Query)

sync_app = Flask(__name__)

@sync_app.route("/graphql", methods=["POST"])
def graphql_server_sync():
data = request.get_json()
result = schema.execute(data["query"], operation_name=data.get("operationName"))
return jsonify(result.data), 200

query = {
"query": "query ErrorQuery { goodbye }",
"operationName": "ErrorQuery",
}
client = sync_app.test_client()
client.post("/graphql", json=query)

assert len(events) == 1

(event,) = events
assert len(event["breadcrumbs"]) == 1

breadcrumbs = event["breadcrumbs"]["values"]
assert len(breadcrumbs) == 1

(breadcrumb,) = breadcrumbs
assert breadcrumb["category"] == "graphql.operation"
assert breadcrumb["data"]["operation_name"] == query["operationName"]
assert breadcrumb["data"]["operation_type"] == "query"
assert breadcrumb["type"] == "default"