sentry-sdk 0.7.5__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.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -30
  2. sentry_sdk/_compat.py +74 -61
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +289 -0
  8. sentry_sdk/_types.py +338 -0
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +134 -0
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +141 -0
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +112 -68
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.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,7 @@
1
+ from .server import ServerInterceptor
2
+ from .client import ClientInterceptor
3
+
4
+ __all__ = [
5
+ "ClientInterceptor",
6
+ "ServerInterceptor",
7
+ ]
@@ -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