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.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -6
  2. sentry_sdk/_compat.py +64 -56
  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 +81 -19
  8. sentry_sdk/_types.py +311 -11
  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 +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  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 +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  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 +35 -28
  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 +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  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 +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  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 +20 -11
  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 +71 -60
  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 +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,69 @@
1
+ """
2
+ Instrumentation for Redis
3
+
4
+ https://github.com/redis/redis-py
5
+ """
6
+
7
+ from sentry_sdk.integrations.redis._sync_common import (
8
+ patch_redis_client,
9
+ patch_redis_pipeline,
10
+ )
11
+ from sentry_sdk.integrations.redis.modules.queries import _set_db_data
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any, Sequence
17
+
18
+
19
+ def _get_redis_command_args(command):
20
+ # type: (Any) -> Sequence[Any]
21
+ return command[0]
22
+
23
+
24
+ def _patch_redis(StrictRedis, client): # noqa: N803
25
+ # type: (Any, Any) -> None
26
+ patch_redis_client(
27
+ StrictRedis,
28
+ is_cluster=False,
29
+ set_db_data_fn=_set_db_data,
30
+ )
31
+ patch_redis_pipeline(
32
+ client.Pipeline,
33
+ is_cluster=False,
34
+ get_command_args_fn=_get_redis_command_args,
35
+ set_db_data_fn=_set_db_data,
36
+ )
37
+ try:
38
+ strict_pipeline = client.StrictPipeline
39
+ except AttributeError:
40
+ pass
41
+ else:
42
+ patch_redis_pipeline(
43
+ strict_pipeline,
44
+ is_cluster=False,
45
+ get_command_args_fn=_get_redis_command_args,
46
+ set_db_data_fn=_set_db_data,
47
+ )
48
+
49
+ try:
50
+ import redis.asyncio
51
+ except ImportError:
52
+ pass
53
+ else:
54
+ from sentry_sdk.integrations.redis._async_common import (
55
+ patch_redis_async_client,
56
+ patch_redis_async_pipeline,
57
+ )
58
+
59
+ patch_redis_async_client(
60
+ redis.asyncio.client.StrictRedis,
61
+ is_cluster=False,
62
+ set_db_data_fn=_set_db_data,
63
+ )
64
+ patch_redis_async_pipeline(
65
+ redis.asyncio.client.Pipeline,
66
+ False,
67
+ _get_redis_command_args,
68
+ set_db_data_fn=_set_db_data,
69
+ )
@@ -0,0 +1,107 @@
1
+ """
2
+ Instrumentation for RedisCluster
3
+ This is part of the main redis-py client.
4
+
5
+ https://github.com/redis/redis-py/blob/master/redis/cluster.py
6
+ """
7
+
8
+ from sentry_sdk.integrations.redis._sync_common import (
9
+ patch_redis_client,
10
+ patch_redis_pipeline,
11
+ )
12
+ from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span
13
+ from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command
14
+
15
+ from sentry_sdk.utils import capture_internal_exceptions
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from typing import Any
21
+ from redis import RedisCluster
22
+ from redis.asyncio.cluster import (
23
+ RedisCluster as AsyncRedisCluster,
24
+ ClusterPipeline as AsyncClusterPipeline,
25
+ )
26
+ from sentry_sdk.tracing import Span
27
+
28
+
29
+ def _set_async_cluster_db_data(span, async_redis_cluster_instance):
30
+ # type: (Span, AsyncRedisCluster[Any]) -> None
31
+ default_node = async_redis_cluster_instance.get_default_node()
32
+ if default_node is not None and default_node.connection_kwargs is not None:
33
+ _set_db_data_on_span(span, default_node.connection_kwargs)
34
+
35
+
36
+ def _set_async_cluster_pipeline_db_data(span, async_redis_cluster_pipeline_instance):
37
+ # type: (Span, AsyncClusterPipeline[Any]) -> None
38
+ with capture_internal_exceptions():
39
+ client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None)
40
+ if client is None:
41
+ # In older redis-py versions, the AsyncClusterPipeline had a `_client`
42
+ # attr but it is private so potentially problematic and mypy does not
43
+ # recognize it - see
44
+ # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386
45
+ client = (
46
+ async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined]
47
+ )
48
+
49
+ _set_async_cluster_db_data(
50
+ span,
51
+ client,
52
+ )
53
+
54
+
55
+ def _set_cluster_db_data(span, redis_cluster_instance):
56
+ # type: (Span, RedisCluster[Any]) -> None
57
+ default_node = redis_cluster_instance.get_default_node()
58
+
59
+ if default_node is not None:
60
+ connection_params = {
61
+ "host": default_node.host,
62
+ "port": default_node.port,
63
+ }
64
+ _set_db_data_on_span(span, connection_params)
65
+
66
+
67
+ def _patch_redis_cluster():
68
+ # type: () -> None
69
+ """Patches the cluster module on redis SDK (as opposed to rediscluster library)"""
70
+ try:
71
+ from redis import RedisCluster, cluster
72
+ except ImportError:
73
+ pass
74
+ else:
75
+ patch_redis_client(
76
+ RedisCluster,
77
+ is_cluster=True,
78
+ set_db_data_fn=_set_cluster_db_data,
79
+ )
80
+ patch_redis_pipeline(
81
+ cluster.ClusterPipeline,
82
+ is_cluster=True,
83
+ get_command_args_fn=_parse_rediscluster_command,
84
+ set_db_data_fn=_set_cluster_db_data,
85
+ )
86
+
87
+ try:
88
+ from redis.asyncio import cluster as async_cluster
89
+ except ImportError:
90
+ pass
91
+ else:
92
+ from sentry_sdk.integrations.redis._async_common import (
93
+ patch_redis_async_client,
94
+ patch_redis_async_pipeline,
95
+ )
96
+
97
+ patch_redis_async_client(
98
+ async_cluster.RedisCluster,
99
+ is_cluster=True,
100
+ set_db_data_fn=_set_async_cluster_db_data,
101
+ )
102
+ patch_redis_async_pipeline(
103
+ async_cluster.ClusterPipeline,
104
+ is_cluster=True,
105
+ get_command_args_fn=_parse_rediscluster_command,
106
+ set_db_data_fn=_set_async_cluster_pipeline_db_data,
107
+ )
@@ -0,0 +1,50 @@
1
+ """
2
+ Instrumentation for redis-py-cluster
3
+ The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021).
4
+
5
+ https://github.com/grokzen/redis-py-cluster
6
+ """
7
+
8
+ from sentry_sdk.integrations.redis._sync_common import (
9
+ patch_redis_client,
10
+ patch_redis_pipeline,
11
+ )
12
+ from sentry_sdk.integrations.redis.modules.queries import _set_db_data
13
+ from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command
14
+
15
+
16
+ def _patch_rediscluster():
17
+ # type: () -> None
18
+ try:
19
+ import rediscluster # type: ignore
20
+ except ImportError:
21
+ return
22
+
23
+ patch_redis_client(
24
+ rediscluster.RedisCluster,
25
+ is_cluster=True,
26
+ set_db_data_fn=_set_db_data,
27
+ )
28
+
29
+ # up to v1.3.6, __version__ attribute is a tuple
30
+ # from v2.0.0, __version__ is a string and VERSION a tuple
31
+ version = getattr(rediscluster, "VERSION", rediscluster.__version__)
32
+
33
+ # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0
34
+ # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst
35
+ if (0, 2, 0) < version < (2, 0, 0):
36
+ pipeline_cls = rediscluster.pipeline.StrictClusterPipeline
37
+ patch_redis_client(
38
+ rediscluster.StrictRedisCluster,
39
+ is_cluster=True,
40
+ set_db_data_fn=_set_db_data,
41
+ )
42
+ else:
43
+ pipeline_cls = rediscluster.pipeline.ClusterPipeline
44
+
45
+ patch_redis_pipeline(
46
+ pipeline_cls,
47
+ is_cluster=True,
48
+ get_command_args_fn=_parse_rediscluster_command,
49
+ set_db_data_fn=_set_db_data,
50
+ )
@@ -0,0 +1,148 @@
1
+ from sentry_sdk.consts import SPANDATA
2
+ from sentry_sdk.integrations.redis.consts import (
3
+ _COMMANDS_INCLUDING_SENSITIVE_DATA,
4
+ _MAX_NUM_ARGS,
5
+ _MAX_NUM_COMMANDS,
6
+ _MULTI_KEY_COMMANDS,
7
+ _SINGLE_KEY_COMMANDS,
8
+ )
9
+ from sentry_sdk.scope import should_send_default_pii
10
+ from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from typing import Any, Optional, Sequence
16
+ from sentry_sdk.tracing import Span
17
+
18
+
19
+ def _get_safe_command(name, args):
20
+ # type: (str, Sequence[Any]) -> str
21
+ command_parts = [name]
22
+
23
+ name_low = name.lower()
24
+ send_default_pii = should_send_default_pii()
25
+
26
+ for i, arg in enumerate(args):
27
+ if i > _MAX_NUM_ARGS:
28
+ break
29
+
30
+ if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA:
31
+ command_parts.append(SENSITIVE_DATA_SUBSTITUTE)
32
+ continue
33
+
34
+ arg_is_the_key = i == 0
35
+ if arg_is_the_key:
36
+ command_parts.append(repr(arg))
37
+ else:
38
+ if send_default_pii:
39
+ command_parts.append(repr(arg))
40
+ else:
41
+ command_parts.append(SENSITIVE_DATA_SUBSTITUTE)
42
+
43
+ command = " ".join(command_parts)
44
+ return command
45
+
46
+
47
+ def _safe_decode(key):
48
+ # type: (Any) -> str
49
+ if isinstance(key, bytes):
50
+ try:
51
+ return key.decode()
52
+ except UnicodeDecodeError:
53
+ return ""
54
+
55
+ return str(key)
56
+
57
+
58
+ def _key_as_string(key):
59
+ # type: (Any) -> str
60
+ if isinstance(key, (dict, list, tuple)):
61
+ key = ", ".join(_safe_decode(x) for x in key)
62
+ elif isinstance(key, bytes):
63
+ key = _safe_decode(key)
64
+ elif key is None:
65
+ key = ""
66
+ else:
67
+ key = str(key)
68
+
69
+ return key
70
+
71
+
72
+ def _get_safe_key(method_name, args, kwargs):
73
+ # type: (str, Optional[tuple[Any, ...]], Optional[dict[str, Any]]) -> Optional[tuple[str, ...]]
74
+ """
75
+ Gets the key (or keys) from the given method_name.
76
+ The method_name could be a redis command or a django caching command
77
+ """
78
+ key = None
79
+
80
+ if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS:
81
+ # for example redis "mget"
82
+ key = tuple(args)
83
+
84
+ elif args is not None and len(args) >= 1:
85
+ # for example django "set_many/get_many" or redis "get"
86
+ if isinstance(args[0], (dict, list, tuple)):
87
+ key = tuple(args[0])
88
+ else:
89
+ key = (args[0],)
90
+
91
+ elif kwargs is not None and "key" in kwargs:
92
+ # this is a legacy case for older versions of Django
93
+ if isinstance(kwargs["key"], (list, tuple)):
94
+ if len(kwargs["key"]) > 0:
95
+ key = tuple(kwargs["key"])
96
+ else:
97
+ if kwargs["key"] is not None:
98
+ key = (kwargs["key"],)
99
+
100
+ return key
101
+
102
+
103
+ def _parse_rediscluster_command(command):
104
+ # type: (Any) -> Sequence[Any]
105
+ return command.args
106
+
107
+
108
+ def _set_pipeline_data(
109
+ span,
110
+ is_cluster,
111
+ get_command_args_fn,
112
+ is_transaction,
113
+ commands_seq,
114
+ ):
115
+ # type: (Span, bool, Any, bool, Sequence[Any]) -> None
116
+ span.set_tag("redis.is_cluster", is_cluster)
117
+ span.set_tag("redis.transaction", is_transaction)
118
+
119
+ commands = []
120
+ for i, arg in enumerate(commands_seq):
121
+ if i >= _MAX_NUM_COMMANDS:
122
+ break
123
+
124
+ command = get_command_args_fn(arg)
125
+ commands.append(_get_safe_command(command[0], command[1:]))
126
+
127
+ span.set_data(
128
+ "redis.commands",
129
+ {
130
+ "count": len(commands_seq),
131
+ "first_ten": commands,
132
+ },
133
+ )
134
+
135
+
136
+ def _set_client_data(span, is_cluster, name, *args):
137
+ # type: (Span, bool, str, *Any) -> None
138
+ span.set_tag("redis.is_cluster", is_cluster)
139
+ if name:
140
+ span.set_tag("redis.command", name)
141
+ span.set_tag(SPANDATA.DB_OPERATION, name)
142
+
143
+ if name and args:
144
+ name_low = name.lower()
145
+ if (name_low in _SINGLE_KEY_COMMANDS) or (
146
+ name_low in _MULTI_KEY_COMMANDS and len(args) == 1
147
+ ):
148
+ span.set_tag("redis.key", args[0])
@@ -1,83 +1,80 @@
1
- from __future__ import absolute_import
2
-
3
1
  import weakref
4
2
 
5
- from sentry_sdk.hub import Hub
6
- from sentry_sdk.integrations import Integration, DidNotEnable
7
- from sentry_sdk.tracing import Transaction
8
- from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
9
-
3
+ import sentry_sdk
4
+ from sentry_sdk.consts import OP
5
+ from sentry_sdk.api import continue_trace
6
+ from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
7
+ from sentry_sdk.integrations.logging import ignore_logger
8
+ from sentry_sdk.tracing import TransactionSource
9
+ from sentry_sdk.utils import (
10
+ capture_internal_exceptions,
11
+ ensure_integration_enabled,
12
+ event_from_exception,
13
+ format_timestamp,
14
+ parse_version,
15
+ )
10
16
 
11
17
  try:
12
- from rq.version import VERSION as RQ_VERSION
18
+ from rq.queue import Queue
13
19
  from rq.timeouts import JobTimeoutException
20
+ from rq.version import VERSION as RQ_VERSION
14
21
  from rq.worker import Worker
15
- from rq.queue import Queue
22
+ from rq.job import JobStatus
16
23
  except ImportError:
17
24
  raise DidNotEnable("RQ not installed")
18
25
 
19
- from sentry_sdk._types import MYPY
20
-
21
- if MYPY:
22
- from typing import Any
23
- from typing import Dict
24
- from typing import Callable
26
+ from typing import TYPE_CHECKING
25
27
 
26
- from rq.job import Job
28
+ if TYPE_CHECKING:
29
+ from typing import Any, Callable
27
30
 
31
+ from sentry_sdk._types import Event, EventProcessor
28
32
  from sentry_sdk.utils import ExcInfo
29
- from sentry_sdk._types import EventProcessor
33
+
34
+ from rq.job import Job
30
35
 
31
36
 
32
37
  class RqIntegration(Integration):
33
38
  identifier = "rq"
39
+ origin = f"auto.queue.{identifier}"
34
40
 
35
41
  @staticmethod
36
42
  def setup_once():
37
43
  # type: () -> None
38
-
39
- try:
40
- version = tuple(map(int, RQ_VERSION.split(".")[:3]))
41
- except (ValueError, TypeError):
42
- raise DidNotEnable("Unparsable RQ version: {}".format(RQ_VERSION))
43
-
44
- if version < (0, 6):
45
- raise DidNotEnable("RQ 0.6 or newer is required.")
44
+ version = parse_version(RQ_VERSION)
45
+ _check_minimum_version(RqIntegration, version)
46
46
 
47
47
  old_perform_job = Worker.perform_job
48
48
 
49
+ @ensure_integration_enabled(RqIntegration, old_perform_job)
49
50
  def sentry_patched_perform_job(self, job, *args, **kwargs):
50
51
  # type: (Any, Job, *Queue, **Any) -> bool
51
- hub = Hub.current
52
- integration = hub.get_integration(RqIntegration)
53
-
54
- if integration is None:
55
- return old_perform_job(self, job, *args, **kwargs)
56
-
57
- client = hub.client
58
- assert client is not None
59
-
60
- with hub.push_scope() as scope:
52
+ with sentry_sdk.new_scope() as scope:
61
53
  scope.clear_breadcrumbs()
62
54
  scope.add_event_processor(_make_event_processor(weakref.ref(job)))
63
55
 
64
- transaction = Transaction.continue_from_headers(
56
+ transaction = continue_trace(
65
57
  job.meta.get("_sentry_trace_headers") or {},
66
- op="rq.task",
58
+ op=OP.QUEUE_TASK_RQ,
67
59
  name="unknown RQ task",
60
+ source=TransactionSource.TASK,
61
+ origin=RqIntegration.origin,
68
62
  )
69
63
 
70
64
  with capture_internal_exceptions():
71
65
  transaction.name = job.func_name
72
66
 
73
- with hub.start_transaction(transaction):
67
+ with sentry_sdk.start_transaction(
68
+ transaction,
69
+ custom_sampling_context={"rq_job": job},
70
+ ):
74
71
  rv = old_perform_job(self, job, *args, **kwargs)
75
72
 
76
73
  if self.is_horse:
77
74
  # We're inside of a forked process and RQ is
78
75
  # about to call `os._exit`. Make sure that our
79
76
  # events get sent out.
80
- client.flush()
77
+ sentry_sdk.get_client().flush()
81
78
 
82
79
  return rv
83
80
 
@@ -87,35 +84,46 @@ class RqIntegration(Integration):
87
84
 
88
85
  def sentry_patched_handle_exception(self, job, *exc_info, **kwargs):
89
86
  # type: (Worker, Any, *Any, **Any) -> Any
90
- _capture_exception(exc_info) # type: ignore
87
+ retry = (
88
+ hasattr(job, "retries_left")
89
+ and job.retries_left
90
+ and job.retries_left > 0
91
+ )
92
+ failed = job._status == JobStatus.FAILED or job.is_failed
93
+ if failed and not retry:
94
+ _capture_exception(exc_info)
95
+
91
96
  return old_handle_exception(self, job, *exc_info, **kwargs)
92
97
 
93
98
  Worker.handle_exception = sentry_patched_handle_exception
94
99
 
95
100
  old_enqueue_job = Queue.enqueue_job
96
101
 
102
+ @ensure_integration_enabled(RqIntegration, old_enqueue_job)
97
103
  def sentry_patched_enqueue_job(self, job, **kwargs):
98
104
  # type: (Queue, Any, **Any) -> Any
99
- hub = Hub.current
100
- if hub.get_integration(RqIntegration) is not None:
105
+ scope = sentry_sdk.get_current_scope()
106
+ if scope.span is not None:
101
107
  job.meta["_sentry_trace_headers"] = dict(
102
- hub.iter_trace_propagation_headers()
108
+ scope.iter_trace_propagation_headers()
103
109
  )
104
110
 
105
111
  return old_enqueue_job(self, job, **kwargs)
106
112
 
107
113
  Queue.enqueue_job = sentry_patched_enqueue_job
108
114
 
115
+ ignore_logger("rq.worker")
116
+
109
117
 
110
118
  def _make_event_processor(weak_job):
111
119
  # type: (Callable[[], Job]) -> EventProcessor
112
120
  def event_processor(event, hint):
113
- # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
121
+ # type: (Event, dict[str, Any]) -> Event
114
122
  job = weak_job()
115
123
  if job is not None:
116
124
  with capture_internal_exceptions():
117
125
  extra = event.setdefault("extra", {})
118
- extra["rq-job"] = {
126
+ rq_job = {
119
127
  "job_id": job.id,
120
128
  "func": job.func_name,
121
129
  "args": job.args,
@@ -123,6 +131,13 @@ def _make_event_processor(weak_job):
123
131
  "description": job.description,
124
132
  }
125
133
 
134
+ if job.enqueued_at:
135
+ rq_job["enqueued_at"] = format_timestamp(job.enqueued_at)
136
+ if job.started_at:
137
+ rq_job["started_at"] = format_timestamp(job.started_at)
138
+
139
+ extra["rq-job"] = rq_job
140
+
126
141
  if "exc_info" in hint:
127
142
  with capture_internal_exceptions():
128
143
  if issubclass(hint["exc_info"][0], JobTimeoutException):
@@ -135,12 +150,7 @@ def _make_event_processor(weak_job):
135
150
 
136
151
  def _capture_exception(exc_info, **kwargs):
137
152
  # type: (ExcInfo, **Any) -> None
138
- hub = Hub.current
139
- if hub.get_integration(RqIntegration) is None:
140
- return
141
-
142
- # If an integration is there, a client has to be there.
143
- client = hub.client # type: Any
153
+ client = sentry_sdk.get_client()
144
154
 
145
155
  event, hint = event_from_exception(
146
156
  exc_info,
@@ -148,4 +158,4 @@ def _capture_exception(exc_info, **kwargs):
148
158
  mechanism={"type": "rq", "handled": False},
149
159
  )
150
160
 
151
- hub.capture_event(event, hint=hint)
161
+ sentry_sdk.capture_event(event, hint=hint)