sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +81 -19
- sentry_sdk/_types.py +311 -11
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +35 -28
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +20 -11
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +71 -60
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +126 -125
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from sentry_sdk.utils import (
|
|
3
|
+
event_from_exception,
|
|
4
|
+
ensure_integration_enabled,
|
|
5
|
+
parse_version,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
|
|
9
|
+
from sentry_sdk.scope import should_send_default_pii
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import gql # type: ignore[import-not-found]
|
|
13
|
+
from graphql import (
|
|
14
|
+
print_ast,
|
|
15
|
+
get_operation_ast,
|
|
16
|
+
DocumentNode,
|
|
17
|
+
VariableDefinitionNode,
|
|
18
|
+
)
|
|
19
|
+
from gql.transport import Transport, AsyncTransport # type: ignore[import-not-found]
|
|
20
|
+
from gql.transport.exceptions import TransportQueryError # type: ignore[import-not-found]
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
# gql 4.0+
|
|
24
|
+
from gql import GraphQLRequest
|
|
25
|
+
except ImportError:
|
|
26
|
+
GraphQLRequest = None
|
|
27
|
+
|
|
28
|
+
except ImportError:
|
|
29
|
+
raise DidNotEnable("gql is not installed")
|
|
30
|
+
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from typing import Any, Dict, Tuple, Union
|
|
35
|
+
from sentry_sdk._types import Event, EventProcessor
|
|
36
|
+
|
|
37
|
+
EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GQLIntegration(Integration):
|
|
41
|
+
identifier = "gql"
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def setup_once():
|
|
45
|
+
# type: () -> None
|
|
46
|
+
gql_version = parse_version(gql.__version__)
|
|
47
|
+
_check_minimum_version(GQLIntegration, gql_version)
|
|
48
|
+
|
|
49
|
+
_patch_execute()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _data_from_document(document):
|
|
53
|
+
# type: (DocumentNode) -> EventDataType
|
|
54
|
+
try:
|
|
55
|
+
operation_ast = get_operation_ast(document)
|
|
56
|
+
data = {"query": print_ast(document)} # type: EventDataType
|
|
57
|
+
|
|
58
|
+
if operation_ast is not None:
|
|
59
|
+
data["variables"] = operation_ast.variable_definitions
|
|
60
|
+
if operation_ast.name is not None:
|
|
61
|
+
data["operationName"] = operation_ast.name.value
|
|
62
|
+
|
|
63
|
+
return data
|
|
64
|
+
except (AttributeError, TypeError):
|
|
65
|
+
return dict()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _transport_method(transport):
|
|
69
|
+
# type: (Union[Transport, AsyncTransport]) -> str
|
|
70
|
+
"""
|
|
71
|
+
The RequestsHTTPTransport allows defining the HTTP method; all
|
|
72
|
+
other transports use POST.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
return transport.method
|
|
76
|
+
except AttributeError:
|
|
77
|
+
return "POST"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _request_info_from_transport(transport):
|
|
81
|
+
# type: (Union[Transport, AsyncTransport, None]) -> Dict[str, str]
|
|
82
|
+
if transport is None:
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
request_info = {
|
|
86
|
+
"method": _transport_method(transport),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
request_info["url"] = transport.url
|
|
91
|
+
except AttributeError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
return request_info
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _patch_execute():
|
|
98
|
+
# type: () -> None
|
|
99
|
+
real_execute = gql.Client.execute
|
|
100
|
+
|
|
101
|
+
@ensure_integration_enabled(GQLIntegration, real_execute)
|
|
102
|
+
def sentry_patched_execute(self, document_or_request, *args, **kwargs):
|
|
103
|
+
# type: (gql.Client, DocumentNode, Any, Any) -> Any
|
|
104
|
+
scope = sentry_sdk.get_isolation_scope()
|
|
105
|
+
scope.add_event_processor(_make_gql_event_processor(self, document_or_request))
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
return real_execute(self, document_or_request, *args, **kwargs)
|
|
109
|
+
except TransportQueryError as e:
|
|
110
|
+
event, hint = event_from_exception(
|
|
111
|
+
e,
|
|
112
|
+
client_options=sentry_sdk.get_client().options,
|
|
113
|
+
mechanism={"type": "gql", "handled": False},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
sentry_sdk.capture_event(event, hint)
|
|
117
|
+
raise e
|
|
118
|
+
|
|
119
|
+
gql.Client.execute = sentry_patched_execute
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _make_gql_event_processor(client, document_or_request):
|
|
123
|
+
# type: (gql.Client, Union[DocumentNode, gql.GraphQLRequest]) -> EventProcessor
|
|
124
|
+
def processor(event, hint):
|
|
125
|
+
# type: (Event, dict[str, Any]) -> Event
|
|
126
|
+
try:
|
|
127
|
+
errors = hint["exc_info"][1].errors
|
|
128
|
+
except (AttributeError, KeyError):
|
|
129
|
+
errors = None
|
|
130
|
+
|
|
131
|
+
request = event.setdefault("request", {})
|
|
132
|
+
request.update(
|
|
133
|
+
{
|
|
134
|
+
"api_target": "graphql",
|
|
135
|
+
**_request_info_from_transport(client.transport),
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if should_send_default_pii():
|
|
140
|
+
if GraphQLRequest is not None and isinstance(
|
|
141
|
+
document_or_request, GraphQLRequest
|
|
142
|
+
):
|
|
143
|
+
# In v4.0.0, gql moved to using GraphQLRequest instead of
|
|
144
|
+
# DocumentNode in execute
|
|
145
|
+
# https://github.com/graphql-python/gql/pull/556
|
|
146
|
+
document = document_or_request.document
|
|
147
|
+
else:
|
|
148
|
+
document = document_or_request
|
|
149
|
+
|
|
150
|
+
request["data"] = _data_from_document(document)
|
|
151
|
+
contexts = event.setdefault("contexts", {})
|
|
152
|
+
response = contexts.setdefault("response", {})
|
|
153
|
+
response.update(
|
|
154
|
+
{
|
|
155
|
+
"data": {"errors": errors},
|
|
156
|
+
"type": response,
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return event
|
|
161
|
+
|
|
162
|
+
return processor
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk.consts import OP
|
|
5
|
+
from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
|
|
6
|
+
from sentry_sdk.scope import should_send_default_pii
|
|
7
|
+
from sentry_sdk.utils import (
|
|
8
|
+
capture_internal_exceptions,
|
|
9
|
+
ensure_integration_enabled,
|
|
10
|
+
event_from_exception,
|
|
11
|
+
package_version,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from graphene.types import schema as graphene_schema # type: ignore
|
|
16
|
+
except ImportError:
|
|
17
|
+
raise DidNotEnable("graphene is not installed")
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Generator
|
|
23
|
+
from typing import Any, Dict, Union
|
|
24
|
+
from graphene.language.source import Source # type: ignore
|
|
25
|
+
from graphql.execution import ExecutionResult
|
|
26
|
+
from graphql.type import GraphQLSchema
|
|
27
|
+
from sentry_sdk._types import Event
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GrapheneIntegration(Integration):
|
|
31
|
+
identifier = "graphene"
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def setup_once():
|
|
35
|
+
# type: () -> None
|
|
36
|
+
version = package_version("graphene")
|
|
37
|
+
_check_minimum_version(GrapheneIntegration, version)
|
|
38
|
+
|
|
39
|
+
_patch_graphql()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _patch_graphql():
|
|
43
|
+
# type: () -> None
|
|
44
|
+
old_graphql_sync = graphene_schema.graphql_sync
|
|
45
|
+
old_graphql_async = graphene_schema.graphql
|
|
46
|
+
|
|
47
|
+
@ensure_integration_enabled(GrapheneIntegration, old_graphql_sync)
|
|
48
|
+
def _sentry_patched_graphql_sync(schema, source, *args, **kwargs):
|
|
49
|
+
# type: (GraphQLSchema, Union[str, Source], Any, Any) -> ExecutionResult
|
|
50
|
+
scope = sentry_sdk.get_isolation_scope()
|
|
51
|
+
scope.add_event_processor(_event_processor)
|
|
52
|
+
|
|
53
|
+
with graphql_span(schema, source, kwargs):
|
|
54
|
+
result = old_graphql_sync(schema, source, *args, **kwargs)
|
|
55
|
+
|
|
56
|
+
with capture_internal_exceptions():
|
|
57
|
+
client = sentry_sdk.get_client()
|
|
58
|
+
for error in result.errors or []:
|
|
59
|
+
event, hint = event_from_exception(
|
|
60
|
+
error,
|
|
61
|
+
client_options=client.options,
|
|
62
|
+
mechanism={
|
|
63
|
+
"type": GrapheneIntegration.identifier,
|
|
64
|
+
"handled": False,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
async def _sentry_patched_graphql_async(schema, source, *args, **kwargs):
|
|
72
|
+
# type: (GraphQLSchema, Union[str, Source], Any, Any) -> ExecutionResult
|
|
73
|
+
integration = sentry_sdk.get_client().get_integration(GrapheneIntegration)
|
|
74
|
+
if integration is None:
|
|
75
|
+
return await old_graphql_async(schema, source, *args, **kwargs)
|
|
76
|
+
|
|
77
|
+
scope = sentry_sdk.get_isolation_scope()
|
|
78
|
+
scope.add_event_processor(_event_processor)
|
|
79
|
+
|
|
80
|
+
with graphql_span(schema, source, kwargs):
|
|
81
|
+
result = await old_graphql_async(schema, source, *args, **kwargs)
|
|
82
|
+
|
|
83
|
+
with capture_internal_exceptions():
|
|
84
|
+
client = sentry_sdk.get_client()
|
|
85
|
+
for error in result.errors or []:
|
|
86
|
+
event, hint = event_from_exception(
|
|
87
|
+
error,
|
|
88
|
+
client_options=client.options,
|
|
89
|
+
mechanism={
|
|
90
|
+
"type": GrapheneIntegration.identifier,
|
|
91
|
+
"handled": False,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
graphene_schema.graphql_sync = _sentry_patched_graphql_sync
|
|
99
|
+
graphene_schema.graphql = _sentry_patched_graphql_async
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _event_processor(event, hint):
|
|
103
|
+
# type: (Event, Dict[str, Any]) -> Event
|
|
104
|
+
if should_send_default_pii():
|
|
105
|
+
request_info = event.setdefault("request", {})
|
|
106
|
+
request_info["api_target"] = "graphql"
|
|
107
|
+
|
|
108
|
+
elif event.get("request", {}).get("data"):
|
|
109
|
+
del event["request"]["data"]
|
|
110
|
+
|
|
111
|
+
return event
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@contextmanager
|
|
115
|
+
def graphql_span(schema, source, kwargs):
|
|
116
|
+
# type: (GraphQLSchema, Union[str, Source], Dict[str, Any]) -> Generator[None, None, None]
|
|
117
|
+
operation_name = kwargs.get("operation_name")
|
|
118
|
+
|
|
119
|
+
operation_type = "query"
|
|
120
|
+
op = OP.GRAPHQL_QUERY
|
|
121
|
+
if source.strip().startswith("mutation"):
|
|
122
|
+
operation_type = "mutation"
|
|
123
|
+
op = OP.GRAPHQL_MUTATION
|
|
124
|
+
elif source.strip().startswith("subscription"):
|
|
125
|
+
operation_type = "subscription"
|
|
126
|
+
op = OP.GRAPHQL_SUBSCRIPTION
|
|
127
|
+
|
|
128
|
+
sentry_sdk.add_breadcrumb(
|
|
129
|
+
crumb={
|
|
130
|
+
"data": {
|
|
131
|
+
"operation_name": operation_name,
|
|
132
|
+
"operation_type": operation_type,
|
|
133
|
+
},
|
|
134
|
+
"category": "graphql.operation",
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
scope = sentry_sdk.get_current_scope()
|
|
139
|
+
if scope.span:
|
|
140
|
+
_graphql_span = scope.span.start_child(op=op, name=operation_name)
|
|
141
|
+
else:
|
|
142
|
+
_graphql_span = sentry_sdk.start_span(op=op, name=operation_name)
|
|
143
|
+
|
|
144
|
+
_graphql_span.set_data("graphql.document", source)
|
|
145
|
+
_graphql_span.set_data("graphql.operation.name", operation_name)
|
|
146
|
+
_graphql_span.set_data("graphql.operation.type", operation_type)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
yield
|
|
150
|
+
finally:
|
|
151
|
+
_graphql_span.finish()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
import grpc
|
|
4
|
+
from grpc import Channel, Server, intercept_channel
|
|
5
|
+
from grpc.aio import Channel as AsyncChannel
|
|
6
|
+
from grpc.aio import Server as AsyncServer
|
|
7
|
+
|
|
8
|
+
from sentry_sdk.integrations import Integration
|
|
9
|
+
from sentry_sdk.utils import parse_version
|
|
10
|
+
|
|
11
|
+
from .client import ClientInterceptor
|
|
12
|
+
from .server import ServerInterceptor
|
|
13
|
+
from .aio.server import ServerInterceptor as AsyncServerInterceptor
|
|
14
|
+
from .aio.client import (
|
|
15
|
+
SentryUnaryUnaryClientInterceptor as AsyncUnaryUnaryClientInterceptor,
|
|
16
|
+
)
|
|
17
|
+
from .aio.client import (
|
|
18
|
+
SentryUnaryStreamClientInterceptor as AsyncUnaryStreamClientIntercetor,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Optional, Sequence
|
|
22
|
+
|
|
23
|
+
# Hack to get new Python features working in older versions
|
|
24
|
+
# without introducing a hard dependency on `typing_extensions`
|
|
25
|
+
# from: https://stackoverflow.com/a/71944042/300572
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from typing import ParamSpec, Callable
|
|
28
|
+
else:
|
|
29
|
+
# Fake ParamSpec
|
|
30
|
+
class ParamSpec:
|
|
31
|
+
def __init__(self, _):
|
|
32
|
+
self.args = None
|
|
33
|
+
self.kwargs = None
|
|
34
|
+
|
|
35
|
+
# Callable[anything] will return None
|
|
36
|
+
class _Callable:
|
|
37
|
+
def __getitem__(self, _):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
# Make instances
|
|
41
|
+
Callable = _Callable()
|
|
42
|
+
|
|
43
|
+
P = ParamSpec("P")
|
|
44
|
+
|
|
45
|
+
GRPC_VERSION = parse_version(grpc.__version__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _wrap_channel_sync(func: Callable[P, Channel]) -> Callable[P, Channel]:
|
|
49
|
+
"Wrapper for synchronous secure and insecure channel."
|
|
50
|
+
|
|
51
|
+
@wraps(func)
|
|
52
|
+
def patched_channel(*args: Any, **kwargs: Any) -> Channel:
|
|
53
|
+
channel = func(*args, **kwargs)
|
|
54
|
+
if not ClientInterceptor._is_intercepted:
|
|
55
|
+
ClientInterceptor._is_intercepted = True
|
|
56
|
+
return intercept_channel(channel, ClientInterceptor())
|
|
57
|
+
else:
|
|
58
|
+
return channel
|
|
59
|
+
|
|
60
|
+
return patched_channel
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _wrap_intercept_channel(func: Callable[P, Channel]) -> Callable[P, Channel]:
|
|
64
|
+
@wraps(func)
|
|
65
|
+
def patched_intercept_channel(
|
|
66
|
+
channel: Channel, *interceptors: grpc.ServerInterceptor
|
|
67
|
+
) -> Channel:
|
|
68
|
+
if ClientInterceptor._is_intercepted:
|
|
69
|
+
interceptors = tuple(
|
|
70
|
+
[
|
|
71
|
+
interceptor
|
|
72
|
+
for interceptor in interceptors
|
|
73
|
+
if not isinstance(interceptor, ClientInterceptor)
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
interceptors = interceptors
|
|
78
|
+
return intercept_channel(channel, *interceptors)
|
|
79
|
+
|
|
80
|
+
return patched_intercept_channel # type: ignore
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _wrap_channel_async(func: Callable[P, AsyncChannel]) -> Callable[P, AsyncChannel]:
|
|
84
|
+
"Wrapper for asynchronous secure and insecure channel."
|
|
85
|
+
|
|
86
|
+
@wraps(func)
|
|
87
|
+
def patched_channel( # type: ignore
|
|
88
|
+
*args: P.args,
|
|
89
|
+
interceptors: Optional[Sequence[grpc.aio.ClientInterceptor]] = None,
|
|
90
|
+
**kwargs: P.kwargs,
|
|
91
|
+
) -> Channel:
|
|
92
|
+
sentry_interceptors = [
|
|
93
|
+
AsyncUnaryUnaryClientInterceptor(),
|
|
94
|
+
AsyncUnaryStreamClientIntercetor(),
|
|
95
|
+
]
|
|
96
|
+
interceptors = [*sentry_interceptors, *(interceptors or [])]
|
|
97
|
+
return func(*args, interceptors=interceptors, **kwargs) # type: ignore
|
|
98
|
+
|
|
99
|
+
return patched_channel # type: ignore
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _wrap_sync_server(func: Callable[P, Server]) -> Callable[P, Server]:
|
|
103
|
+
"""Wrapper for synchronous server."""
|
|
104
|
+
|
|
105
|
+
@wraps(func)
|
|
106
|
+
def patched_server( # type: ignore
|
|
107
|
+
*args: P.args,
|
|
108
|
+
interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
|
|
109
|
+
**kwargs: P.kwargs,
|
|
110
|
+
) -> Server:
|
|
111
|
+
interceptors = [
|
|
112
|
+
interceptor
|
|
113
|
+
for interceptor in interceptors or []
|
|
114
|
+
if not isinstance(interceptor, ServerInterceptor)
|
|
115
|
+
]
|
|
116
|
+
server_interceptor = ServerInterceptor()
|
|
117
|
+
interceptors = [server_interceptor, *(interceptors or [])]
|
|
118
|
+
return func(*args, interceptors=interceptors, **kwargs) # type: ignore
|
|
119
|
+
|
|
120
|
+
return patched_server # type: ignore
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _wrap_async_server(func: Callable[P, AsyncServer]) -> Callable[P, AsyncServer]:
|
|
124
|
+
"""Wrapper for asynchronous server."""
|
|
125
|
+
|
|
126
|
+
@wraps(func)
|
|
127
|
+
def patched_aio_server( # type: ignore
|
|
128
|
+
*args: P.args,
|
|
129
|
+
interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
|
|
130
|
+
**kwargs: P.kwargs,
|
|
131
|
+
) -> Server:
|
|
132
|
+
server_interceptor = AsyncServerInterceptor()
|
|
133
|
+
interceptors = [
|
|
134
|
+
server_interceptor,
|
|
135
|
+
*(interceptors or []),
|
|
136
|
+
] # type: Sequence[grpc.ServerInterceptor]
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# We prefer interceptors as a list because of compatibility with
|
|
140
|
+
# opentelemetry https://github.com/getsentry/sentry-python/issues/4389
|
|
141
|
+
# However, prior to grpc 1.42.0, only tuples were accepted, so we
|
|
142
|
+
# have no choice there.
|
|
143
|
+
if GRPC_VERSION is not None and GRPC_VERSION < (1, 42, 0):
|
|
144
|
+
interceptors = tuple(interceptors)
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
return func(*args, interceptors=interceptors, **kwargs) # type: ignore
|
|
149
|
+
|
|
150
|
+
return patched_aio_server # type: ignore
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class GRPCIntegration(Integration):
|
|
154
|
+
identifier = "grpc"
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def setup_once() -> None:
|
|
158
|
+
import grpc
|
|
159
|
+
|
|
160
|
+
grpc.insecure_channel = _wrap_channel_sync(grpc.insecure_channel)
|
|
161
|
+
grpc.secure_channel = _wrap_channel_sync(grpc.secure_channel)
|
|
162
|
+
grpc.intercept_channel = _wrap_intercept_channel(grpc.intercept_channel)
|
|
163
|
+
|
|
164
|
+
grpc.aio.insecure_channel = _wrap_channel_async(grpc.aio.insecure_channel)
|
|
165
|
+
grpc.aio.secure_channel = _wrap_channel_async(grpc.aio.secure_channel)
|
|
166
|
+
|
|
167
|
+
grpc.server = _wrap_sync_server(grpc.server)
|
|
168
|
+
grpc.aio.server = _wrap_async_server(grpc.aio.server)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from typing import Callable, Union, AsyncIterable, Any
|
|
2
|
+
|
|
3
|
+
from grpc.aio import (
|
|
4
|
+
UnaryUnaryClientInterceptor,
|
|
5
|
+
UnaryStreamClientInterceptor,
|
|
6
|
+
ClientCallDetails,
|
|
7
|
+
UnaryUnaryCall,
|
|
8
|
+
UnaryStreamCall,
|
|
9
|
+
Metadata,
|
|
10
|
+
)
|
|
11
|
+
from google.protobuf.message import Message
|
|
12
|
+
|
|
13
|
+
import sentry_sdk
|
|
14
|
+
from sentry_sdk.consts import OP
|
|
15
|
+
from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClientInterceptor:
|
|
19
|
+
@staticmethod
|
|
20
|
+
def _update_client_call_details_metadata_from_scope(
|
|
21
|
+
client_call_details: ClientCallDetails,
|
|
22
|
+
) -> ClientCallDetails:
|
|
23
|
+
if client_call_details.metadata is None:
|
|
24
|
+
client_call_details = client_call_details._replace(metadata=Metadata())
|
|
25
|
+
elif not isinstance(client_call_details.metadata, Metadata):
|
|
26
|
+
# This is a workaround for a GRPC bug, which was fixed in grpcio v1.60.0
|
|
27
|
+
# See https://github.com/grpc/grpc/issues/34298.
|
|
28
|
+
client_call_details = client_call_details._replace(
|
|
29
|
+
metadata=Metadata.from_tuple(client_call_details.metadata)
|
|
30
|
+
)
|
|
31
|
+
for (
|
|
32
|
+
key,
|
|
33
|
+
value,
|
|
34
|
+
) in sentry_sdk.get_current_scope().iter_trace_propagation_headers():
|
|
35
|
+
client_call_details.metadata.add(key, value)
|
|
36
|
+
return client_call_details
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SentryUnaryUnaryClientInterceptor(ClientInterceptor, UnaryUnaryClientInterceptor): # type: ignore
|
|
40
|
+
async def intercept_unary_unary(
|
|
41
|
+
self,
|
|
42
|
+
continuation: Callable[[ClientCallDetails, Message], UnaryUnaryCall],
|
|
43
|
+
client_call_details: ClientCallDetails,
|
|
44
|
+
request: Message,
|
|
45
|
+
) -> Union[UnaryUnaryCall, Message]:
|
|
46
|
+
method = client_call_details.method
|
|
47
|
+
|
|
48
|
+
with sentry_sdk.start_span(
|
|
49
|
+
op=OP.GRPC_CLIENT,
|
|
50
|
+
name="unary unary call to %s" % method.decode(),
|
|
51
|
+
origin=SPAN_ORIGIN,
|
|
52
|
+
) as span:
|
|
53
|
+
span.set_data("type", "unary unary")
|
|
54
|
+
span.set_data("method", method)
|
|
55
|
+
|
|
56
|
+
client_call_details = self._update_client_call_details_metadata_from_scope(
|
|
57
|
+
client_call_details
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
response = await continuation(client_call_details, request)
|
|
61
|
+
status_code = await response.code()
|
|
62
|
+
span.set_data("code", status_code.name)
|
|
63
|
+
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SentryUnaryStreamClientInterceptor(
|
|
68
|
+
ClientInterceptor,
|
|
69
|
+
UnaryStreamClientInterceptor, # type: ignore
|
|
70
|
+
):
|
|
71
|
+
async def intercept_unary_stream(
|
|
72
|
+
self,
|
|
73
|
+
continuation: Callable[[ClientCallDetails, Message], UnaryStreamCall],
|
|
74
|
+
client_call_details: ClientCallDetails,
|
|
75
|
+
request: Message,
|
|
76
|
+
) -> Union[AsyncIterable[Any], UnaryStreamCall]:
|
|
77
|
+
method = client_call_details.method
|
|
78
|
+
|
|
79
|
+
with sentry_sdk.start_span(
|
|
80
|
+
op=OP.GRPC_CLIENT,
|
|
81
|
+
name="unary stream call to %s" % method.decode(),
|
|
82
|
+
origin=SPAN_ORIGIN,
|
|
83
|
+
) as span:
|
|
84
|
+
span.set_data("type", "unary stream")
|
|
85
|
+
span.set_data("method", method)
|
|
86
|
+
|
|
87
|
+
client_call_details = self._update_client_call_details_metadata_from_scope(
|
|
88
|
+
client_call_details
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
response = await continuation(client_call_details, request)
|
|
92
|
+
# status_code = await response.code()
|
|
93
|
+
# span.set_data("code", status_code)
|
|
94
|
+
|
|
95
|
+
return response
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import sentry_sdk
|
|
2
|
+
from sentry_sdk.consts import OP
|
|
3
|
+
from sentry_sdk.integrations import DidNotEnable
|
|
4
|
+
from sentry_sdk.integrations.grpc.consts import SPAN_ORIGIN
|
|
5
|
+
from sentry_sdk.tracing import Transaction, TransactionSource
|
|
6
|
+
from sentry_sdk.utils import event_from_exception
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import grpc
|
|
17
|
+
from grpc import HandlerCallDetails, RpcMethodHandler
|
|
18
|
+
from grpc.aio import AbortError, ServicerContext
|
|
19
|
+
except ImportError:
|
|
20
|
+
raise DidNotEnable("grpcio is not installed")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ServerInterceptor(grpc.aio.ServerInterceptor): # type: ignore
|
|
24
|
+
def __init__(self, find_name=None):
|
|
25
|
+
# type: (ServerInterceptor, Callable[[ServicerContext], str] | None) -> None
|
|
26
|
+
self._find_method_name = find_name or self._find_name
|
|
27
|
+
|
|
28
|
+
super().__init__()
|
|
29
|
+
|
|
30
|
+
async def intercept_service(self, continuation, handler_call_details):
|
|
31
|
+
# type: (ServerInterceptor, Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]], HandlerCallDetails) -> Optional[Awaitable[RpcMethodHandler]]
|
|
32
|
+
self._handler_call_details = handler_call_details
|
|
33
|
+
handler = await continuation(handler_call_details)
|
|
34
|
+
if handler is None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
if not handler.request_streaming and not handler.response_streaming:
|
|
38
|
+
handler_factory = grpc.unary_unary_rpc_method_handler
|
|
39
|
+
|
|
40
|
+
async def wrapped(request, context):
|
|
41
|
+
# type: (Any, ServicerContext) -> Any
|
|
42
|
+
name = self._find_method_name(context)
|
|
43
|
+
if not name:
|
|
44
|
+
return await handler(request, context)
|
|
45
|
+
|
|
46
|
+
# What if the headers are empty?
|
|
47
|
+
transaction = Transaction.continue_from_headers(
|
|
48
|
+
dict(context.invocation_metadata()),
|
|
49
|
+
op=OP.GRPC_SERVER,
|
|
50
|
+
name=name,
|
|
51
|
+
source=TransactionSource.CUSTOM,
|
|
52
|
+
origin=SPAN_ORIGIN,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
with sentry_sdk.start_transaction(transaction=transaction):
|
|
56
|
+
try:
|
|
57
|
+
return await handler.unary_unary(request, context)
|
|
58
|
+
except AbortError:
|
|
59
|
+
raise
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
event, hint = event_from_exception(
|
|
62
|
+
exc,
|
|
63
|
+
mechanism={"type": "grpc", "handled": False},
|
|
64
|
+
)
|
|
65
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
elif not handler.request_streaming and handler.response_streaming:
|
|
69
|
+
handler_factory = grpc.unary_stream_rpc_method_handler
|
|
70
|
+
|
|
71
|
+
async def wrapped(request, context): # type: ignore
|
|
72
|
+
# type: (Any, ServicerContext) -> Any
|
|
73
|
+
async for r in handler.unary_stream(request, context):
|
|
74
|
+
yield r
|
|
75
|
+
|
|
76
|
+
elif handler.request_streaming and not handler.response_streaming:
|
|
77
|
+
handler_factory = grpc.stream_unary_rpc_method_handler
|
|
78
|
+
|
|
79
|
+
async def wrapped(request, context):
|
|
80
|
+
# type: (Any, ServicerContext) -> Any
|
|
81
|
+
response = handler.stream_unary(request, context)
|
|
82
|
+
return await response
|
|
83
|
+
|
|
84
|
+
elif handler.request_streaming and handler.response_streaming:
|
|
85
|
+
handler_factory = grpc.stream_stream_rpc_method_handler
|
|
86
|
+
|
|
87
|
+
async def wrapped(request, context): # type: ignore
|
|
88
|
+
# type: (Any, ServicerContext) -> Any
|
|
89
|
+
async for r in handler.stream_stream(request, context):
|
|
90
|
+
yield r
|
|
91
|
+
|
|
92
|
+
return handler_factory(
|
|
93
|
+
wrapped,
|
|
94
|
+
request_deserializer=handler.request_deserializer,
|
|
95
|
+
response_serializer=handler.response_serializer,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _find_name(self, context):
|
|
99
|
+
# type: (ServicerContext) -> str
|
|
100
|
+
return self._handler_call_details.method
|