Skip to content

gql: execute() leaves its event processor on the isolation scope, so later unrelated errors are reported as GraphQL requests #8262

Open

Description

@Mohil-Ahuja

How do you use Sentry?

Sentry Saas (sentry.io)

Version

master (860a060), gql 4.4.0

Steps to Reproduce

sentry_patched_execute adds an event processor to the isolation scope on every Client.execute() and never removes it:

scope = sentry_sdk.get_isolation_scope()
scope.add_event_processor(_make_gql_event_processor(self, document))

gql is a client, so it's often called outside a per-request isolation scope (scripts, workers, task loops), or many times inside one request. The processors outlive the query and apply to every later event on that scope.

import sentry_sdk
from gql import Client, gql
from gql.transport.requests import RequestsHTTPTransport
from sentry_sdk.integrations.gql import GQLIntegration

sentry_sdk.init(dsn=..., integrations=[GQLIntegration()], send_default_pii=True)
client = Client(transport=RequestsHTTPTransport(url="http://example.com/graphql"))

for i in range(100):
    client.execute(gql("query Q%d { example }" % i))  # all succeed

# later, unrelated to GraphQL
try:
    raise ValueError("unrelated error")
except ValueError:
    sentry_sdk.capture_exception()

Full repro (HTTP mocked with responses, events captured from a custom transport): https://github.com/Mohil-Ahuja/ci-scratch/blob/main/repro/gql_processors.py, run output: https://github.com/Mohil-Ahuja/ci-scratch/actions/runs/42837565235

Expected Result

The unrelated ValueError has no GraphQL request data, and contexts.response.type is "response" on gql error events.

Actual Result

  1. Unrelated errors are tagged as GraphQL requests. The ValueError above is sent with the last query attached:

    "request": {"api_target": "graphql", "method": "POST", "url": "http://example.com/graphql",
                "data": {"query": "query Q99 {\n  example\n}", "variables": null, "operationName": "Q99"}}
  2. Other processors on the scope get wiped. Once a scope has more than 20 processors, Scope.add_event_processor clears all of them. So 21 gql calls in one request remove processors added by the app or other integrations. In the repro, a processor added to the same isolation scope before 21 queries is gone afterwards, and the next error event is missing its tag.

  3. contexts.response.type is a self-reference. Both branches of the processor do response.update({"data": ..., "type": response}), which sets type to the dict itself, so it's serialized as "<cyclic>":

    "response": {"data": {"errors": [{"message": "boom"}]}, "type": "<cyclic>"}

Possible fix: fork a scope around real_execute (with sentry_sdk.new_scope() as scope:) and add the processor there, so it only applies to events captured during the query (including the TransportQueryError event), and set "type": "response". Happy to open a PR with tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions