File size: 5,042 Bytes
9c6594c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
from contextlib import contextmanager

import sentry_sdk
from sentry_sdk.consts import OP
from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.utils import (
    capture_internal_exceptions,
    ensure_integration_enabled,
    event_from_exception,
    package_version,
)

try:
    from graphene.types import schema as graphene_schema  # type: ignore
except ImportError:
    raise DidNotEnable("graphene is not installed")

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Generator
    from typing import Any, Dict, Union
    from graphene.language.source import Source  # type: ignore
    from graphql.execution import ExecutionResult
    from graphql.type import GraphQLSchema
    from sentry_sdk._types import Event


class GrapheneIntegration(Integration):
    identifier = "graphene"

    @staticmethod
    def setup_once():
        # type: () -> None
        version = package_version("graphene")
        _check_minimum_version(GrapheneIntegration, version)

        _patch_graphql()


def _patch_graphql():
    # type: () -> None
    old_graphql_sync = graphene_schema.graphql_sync
    old_graphql_async = graphene_schema.graphql

    @ensure_integration_enabled(GrapheneIntegration, old_graphql_sync)
    def _sentry_patched_graphql_sync(schema, source, *args, **kwargs):
        # type: (GraphQLSchema, Union[str, Source], Any, Any) -> ExecutionResult
        scope = sentry_sdk.get_isolation_scope()
        scope.add_event_processor(_event_processor)

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

        with capture_internal_exceptions():
            client = sentry_sdk.get_client()
            for error in result.errors or []:
                event, hint = event_from_exception(
                    error,
                    client_options=client.options,
                    mechanism={
                        "type": GrapheneIntegration.identifier,
                        "handled": False,
                    },
                )
                sentry_sdk.capture_event(event, hint=hint)

        return result

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

        scope = sentry_sdk.get_isolation_scope()
        scope.add_event_processor(_event_processor)

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

        with capture_internal_exceptions():
            client = sentry_sdk.get_client()
            for error in result.errors or []:
                event, hint = event_from_exception(
                    error,
                    client_options=client.options,
                    mechanism={
                        "type": GrapheneIntegration.identifier,
                        "handled": False,
                    },
                )
                sentry_sdk.capture_event(event, hint=hint)

        return result

    graphene_schema.graphql_sync = _sentry_patched_graphql_sync
    graphene_schema.graphql = _sentry_patched_graphql_async


def _event_processor(event, hint):
    # type: (Event, Dict[str, Any]) -> Event
    if should_send_default_pii():
        request_info = event.setdefault("request", {})
        request_info["api_target"] = "graphql"

    elif event.get("request", {}).get("data"):
        del event["request"]["data"]

    return event


@contextmanager
def graphql_span(schema, source, kwargs):
    # type: (GraphQLSchema, Union[str, Source], Dict[str, Any]) -> Generator[None, None, None]
    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

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

    scope = sentry_sdk.get_current_scope()
    if scope.span:
        _graphql_span = scope.span.start_child(op=op, name=operation_name)
    else:
        _graphql_span = sentry_sdk.start_span(op=op, name=operation_name)

    _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)

    try:
        yield
    finally:
        _graphql_span.finish()