Skip to content

Commit 91ec64b

Browse files
authored
feat(ariadne): Gate GraphQL data collection behind data_collection option (#6890)
Filter the GraphQL document and variables attached to error events based on the structured data_collection configuration (graphql.document / graphql.variables), falling back to send_default_pii when data_collection is not set. The document remains subject to max_request_body_size; variables are not. When both are disabled, any request data attached by other integrations is scrubbed. Response body capture remains tied to send_default_pii. Refs PY-2585 Refs #6745
1 parent 258a965 commit 91ec64b

2 files changed

Lines changed: 192 additions & 2 deletions

File tree

sentry_sdk/integrations/ariadne.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
capture_internal_exceptions,
1111
ensure_integration_enabled,
1212
event_from_exception,
13+
has_data_collection_enabled,
1314
package_version,
1415
)
1516

@@ -135,8 +136,28 @@ def inner(event: "Event", hint: "dict[str, Any]") -> "Event":
135136
)
136137
except (TypeError, ValueError):
137138
return event
138-
139-
if should_send_default_pii() and request_body_within_bounds(
139+
client_options = sentry_sdk.get_client().options
140+
141+
if has_data_collection_enabled(client_options):
142+
dc_graphql = client_options["data_collection"]["graphql"]
143+
collect_variables = dc_graphql["variables"]
144+
collect_document = dc_graphql[
145+
"document"
146+
] and request_body_within_bounds(get_client(), content_length)
147+
148+
if collect_document or collect_variables:
149+
request_info = event.setdefault("request", {})
150+
request_info["api_target"] = "graphql"
151+
request_info["data"] = {
152+
key: value
153+
for key, value in data.items()
154+
if (key != "query" or collect_document)
155+
and (key != "variables" or collect_variables)
156+
}
157+
elif event.get("request", {}).get("data"):
158+
del event["request"]["data"]
159+
160+
elif should_send_default_pii() and request_body_within_bounds(
140161
get_client(), content_length
141162
):
142163
request_info = event.setdefault("request", {})

tests/integrations/ariadne/test_ariadne.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import pytest
12
from ariadne import ObjectType, QueryType, gql, graphql_sync, make_executable_schema
23
from ariadne.asgi import GraphQL
34
from fastapi import FastAPI
45
from fastapi.testclient import TestClient
56
from flask import Flask, jsonify, request
7+
from flask import request as flask_request
68

79
from sentry_sdk.integrations.ariadne import AriadneIntegration
810
from sentry_sdk.integrations.fastapi import FastApiIntegration
@@ -274,3 +276,170 @@ def graphql_server():
274276
client.post("/graphql", json=query)
275277

276278
assert len(events) == 0
279+
280+
281+
ERROR_QUERY_WITH_VARIABLES = {
282+
"query": (
283+
"query GreetingQuery($name: String) { greeting(name: $name) {name} error }"
284+
),
285+
"variables": {"name": "some name"},
286+
}
287+
288+
289+
@pytest.fixture(params=["flask", "fastapi"])
290+
def graphql_client(request):
291+
"""Build a test client for each supported framework, hitting an endpoint
292+
whose resolver raises so an event is always captured."""
293+
294+
def make_client():
295+
schema = schema_factory()
296+
if request.param == "flask":
297+
app = Flask(__name__)
298+
299+
@app.route("/graphql", methods=["POST"])
300+
def graphql_server():
301+
success, result = graphql_sync(schema, flask_request.get_json())
302+
return jsonify(result), 200
303+
304+
return app.test_client()
305+
306+
async_app = FastAPI()
307+
async_app.mount("/graphql/", GraphQL(schema))
308+
return TestClient(async_app)
309+
310+
return make_client
311+
312+
313+
def _init_all_integrations(sentry_init, **kwargs):
314+
sentry_init(
315+
integrations=[
316+
AriadneIntegration(),
317+
FlaskIntegration(),
318+
FastApiIntegration(),
319+
StarletteIntegration(),
320+
],
321+
**kwargs,
322+
)
323+
324+
325+
@pytest.mark.parametrize(
326+
"init_kwargs,expect_query,expect_variables",
327+
[
328+
pytest.param(
329+
{"_experiments": {"data_collection": {}}},
330+
True,
331+
True,
332+
id="data_collection_defaults",
333+
),
334+
pytest.param(
335+
{
336+
"_experiments": {
337+
"data_collection": {
338+
"graphql": {"document": True, "variables": True}
339+
}
340+
}
341+
},
342+
True,
343+
True,
344+
id="document_on_variables_on",
345+
),
346+
pytest.param(
347+
{"_experiments": {"data_collection": {"graphql": {"document": False}}}},
348+
False,
349+
True,
350+
id="document_off_variables_on",
351+
),
352+
pytest.param(
353+
{"_experiments": {"data_collection": {"graphql": {"variables": False}}}},
354+
True,
355+
False,
356+
id="document_on_variables_off",
357+
),
358+
pytest.param(
359+
{
360+
"_experiments": {
361+
"data_collection": {
362+
"graphql": {"document": False, "variables": False}
363+
}
364+
}
365+
},
366+
None,
367+
None,
368+
id="document_off_variables_off",
369+
),
370+
pytest.param(
371+
{
372+
"send_default_pii": True,
373+
"_experiments": {"data_collection": {"graphql": {"document": False}}},
374+
},
375+
False,
376+
True,
377+
id="data_collection_takes_precedence_over_send_default_pii_on",
378+
),
379+
pytest.param(
380+
{
381+
"send_default_pii": False,
382+
"_experiments": {"data_collection": {"graphql": {"document": True}}},
383+
},
384+
True,
385+
True,
386+
id="data_collection_takes_precedence_over_send_default_pii_off",
387+
),
388+
],
389+
)
390+
def test_request_data_collection(
391+
sentry_init,
392+
capture_events,
393+
graphql_client,
394+
init_kwargs,
395+
expect_query,
396+
expect_variables,
397+
):
398+
_init_all_integrations(sentry_init, **init_kwargs)
399+
events = capture_events()
400+
401+
graphql_client().post("/graphql", json=ERROR_QUERY_WITH_VARIABLES)
402+
403+
assert len(events) == 1
404+
(event,) = events
405+
406+
if expect_query is None:
407+
assert "data" not in event["request"]
408+
return
409+
410+
assert event["request"]["api_target"] == "graphql"
411+
assert ("query" in event["request"]["data"]) == expect_query
412+
assert ("variables" in event["request"]["data"]) == expect_variables
413+
414+
# Response body capture is intentionally tied to send_default_pii only.
415+
assert ("response" in event["contexts"]) == bool(
416+
init_kwargs.get("send_default_pii")
417+
)
418+
419+
420+
def test_request_data_collection_body_out_of_bounds_still_collects_variables(
421+
sentry_init, capture_events, graphql_client
422+
):
423+
"""
424+
When the request body exceeds ``max_request_body_size``, the document is
425+
dropped but variables (which are not subject to the bounds check) are
426+
still collected.
427+
"""
428+
_init_all_integrations(
429+
sentry_init,
430+
max_request_body_size="small",
431+
_experiments={"data_collection": {}},
432+
)
433+
events = capture_events()
434+
435+
query = dict(ERROR_QUERY_WITH_VARIABLES)
436+
# The integration reads Content-Length from the payload's "headers" key;
437+
# report a size over the "small" limit (10**3).
438+
query["headers"] = {"Content-Length": str(10**4)}
439+
graphql_client().post("/graphql", json=query)
440+
441+
assert len(events) == 1
442+
(event,) = events
443+
444+
assert "query" not in event["request"]["data"]
445+
assert event["request"]["data"]["variables"] == {"name": "some name"}

0 commit comments

Comments
 (0)