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,10 @@
1
+ from sentry_sdk.crons.api import capture_checkin
2
+ from sentry_sdk.crons.consts import MonitorStatus
3
+ from sentry_sdk.crons.decorator import monitor
4
+
5
+
6
+ __all__ = [
7
+ "capture_checkin",
8
+ "MonitorStatus",
9
+ "monitor",
10
+ ]
@@ -0,0 +1,62 @@
1
+ import uuid
2
+
3
+ import sentry_sdk
4
+ from sentry_sdk.utils import logger
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import Optional
10
+ from sentry_sdk._types import Event, MonitorConfig
11
+
12
+
13
+ def _create_check_in_event(
14
+ monitor_slug=None, # type: Optional[str]
15
+ check_in_id=None, # type: Optional[str]
16
+ status=None, # type: Optional[str]
17
+ duration_s=None, # type: Optional[float]
18
+ monitor_config=None, # type: Optional[MonitorConfig]
19
+ ):
20
+ # type: (...) -> Event
21
+ options = sentry_sdk.get_client().options
22
+ check_in_id = check_in_id or uuid.uuid4().hex # type: str
23
+
24
+ check_in = {
25
+ "type": "check_in",
26
+ "monitor_slug": monitor_slug,
27
+ "check_in_id": check_in_id,
28
+ "status": status,
29
+ "duration": duration_s,
30
+ "environment": options.get("environment", None),
31
+ "release": options.get("release", None),
32
+ } # type: Event
33
+
34
+ if monitor_config:
35
+ check_in["monitor_config"] = monitor_config
36
+
37
+ return check_in
38
+
39
+
40
+ def capture_checkin(
41
+ monitor_slug=None, # type: Optional[str]
42
+ check_in_id=None, # type: Optional[str]
43
+ status=None, # type: Optional[str]
44
+ duration=None, # type: Optional[float]
45
+ monitor_config=None, # type: Optional[MonitorConfig]
46
+ ):
47
+ # type: (...) -> str
48
+ check_in_event = _create_check_in_event(
49
+ monitor_slug=monitor_slug,
50
+ check_in_id=check_in_id,
51
+ status=status,
52
+ duration_s=duration,
53
+ monitor_config=monitor_config,
54
+ )
55
+
56
+ sentry_sdk.capture_event(check_in_event)
57
+
58
+ logger.debug(
59
+ f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}"
60
+ )
61
+
62
+ return check_in_event["check_in_id"]
@@ -0,0 +1,4 @@
1
+ class MonitorStatus:
2
+ IN_PROGRESS = "in_progress"
3
+ OK = "ok"
4
+ ERROR = "error"
@@ -0,0 +1,135 @@
1
+ from functools import wraps
2
+ from inspect import iscoroutinefunction
3
+
4
+ from sentry_sdk.crons import capture_checkin
5
+ from sentry_sdk.crons.consts import MonitorStatus
6
+ from sentry_sdk.utils import now
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Awaitable, Callable
12
+ from types import TracebackType
13
+ from typing import (
14
+ Any,
15
+ Optional,
16
+ ParamSpec,
17
+ Type,
18
+ TypeVar,
19
+ Union,
20
+ cast,
21
+ overload,
22
+ )
23
+ from sentry_sdk._types import MonitorConfig
24
+
25
+ P = ParamSpec("P")
26
+ R = TypeVar("R")
27
+
28
+
29
+ class monitor: # noqa: N801
30
+ """
31
+ Decorator/context manager to capture checkin events for a monitor.
32
+
33
+ Usage (as decorator):
34
+ ```
35
+ import sentry_sdk
36
+
37
+ app = Celery()
38
+
39
+ @app.task
40
+ @sentry_sdk.monitor(monitor_slug='my-fancy-slug')
41
+ def test(arg):
42
+ print(arg)
43
+ ```
44
+
45
+ This does not have to be used with Celery, but if you do use it with celery,
46
+ put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator.
47
+
48
+ Usage (as context manager):
49
+ ```
50
+ import sentry_sdk
51
+
52
+ def test(arg):
53
+ with sentry_sdk.monitor(monitor_slug='my-fancy-slug'):
54
+ print(arg)
55
+ ```
56
+ """
57
+
58
+ def __init__(self, monitor_slug=None, monitor_config=None):
59
+ # type: (Optional[str], Optional[MonitorConfig]) -> None
60
+ self.monitor_slug = monitor_slug
61
+ self.monitor_config = monitor_config
62
+
63
+ def __enter__(self):
64
+ # type: () -> None
65
+ self.start_timestamp = now()
66
+ self.check_in_id = capture_checkin(
67
+ monitor_slug=self.monitor_slug,
68
+ status=MonitorStatus.IN_PROGRESS,
69
+ monitor_config=self.monitor_config,
70
+ )
71
+
72
+ def __exit__(self, exc_type, exc_value, traceback):
73
+ # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None
74
+ duration_s = now() - self.start_timestamp
75
+
76
+ if exc_type is None and exc_value is None and traceback is None:
77
+ status = MonitorStatus.OK
78
+ else:
79
+ status = MonitorStatus.ERROR
80
+
81
+ capture_checkin(
82
+ monitor_slug=self.monitor_slug,
83
+ check_in_id=self.check_in_id,
84
+ status=status,
85
+ duration=duration_s,
86
+ monitor_config=self.monitor_config,
87
+ )
88
+
89
+ if TYPE_CHECKING:
90
+
91
+ @overload
92
+ def __call__(self, fn):
93
+ # type: (Callable[P, Awaitable[Any]]) -> Callable[P, Awaitable[Any]]
94
+ # Unfortunately, mypy does not give us any reliable way to type check the
95
+ # return value of an Awaitable (i.e. async function) for this overload,
96
+ # since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]].
97
+ ...
98
+
99
+ @overload
100
+ def __call__(self, fn):
101
+ # type: (Callable[P, R]) -> Callable[P, R]
102
+ ...
103
+
104
+ def __call__(
105
+ self,
106
+ fn, # type: Union[Callable[P, R], Callable[P, Awaitable[Any]]]
107
+ ):
108
+ # type: (...) -> Union[Callable[P, R], Callable[P, Awaitable[Any]]]
109
+ if iscoroutinefunction(fn):
110
+ return self._async_wrapper(fn)
111
+
112
+ else:
113
+ if TYPE_CHECKING:
114
+ fn = cast("Callable[P, R]", fn)
115
+ return self._sync_wrapper(fn)
116
+
117
+ def _async_wrapper(self, fn):
118
+ # type: (Callable[P, Awaitable[Any]]) -> Callable[P, Awaitable[Any]]
119
+ @wraps(fn)
120
+ async def inner(*args: "P.args", **kwargs: "P.kwargs"):
121
+ # type: (...) -> R
122
+ with self:
123
+ return await fn(*args, **kwargs)
124
+
125
+ return inner
126
+
127
+ def _sync_wrapper(self, fn):
128
+ # type: (Callable[P, R]) -> Callable[P, R]
129
+ @wraps(fn)
130
+ def inner(*args: "P.args", **kwargs: "P.kwargs"):
131
+ # type: (...) -> R
132
+ with self:
133
+ return fn(*args, **kwargs)
134
+
135
+ return inner
sentry_sdk/debug.py CHANGED
@@ -1,29 +1,26 @@
1
1
  import sys
2
2
  import logging
3
+ import warnings
3
4
 
4
- from sentry_sdk import utils
5
- from sentry_sdk.hub import Hub
6
- from sentry_sdk.utils import logger
5
+ from sentry_sdk import get_client
7
6
  from sentry_sdk.client import _client_init_debug
7
+ from sentry_sdk.utils import logger
8
8
  from logging import LogRecord
9
9
 
10
10
 
11
- class _HubBasedClientFilter(logging.Filter):
11
+ class _DebugFilter(logging.Filter):
12
12
  def filter(self, record):
13
13
  # type: (LogRecord) -> bool
14
14
  if _client_init_debug.get(False):
15
15
  return True
16
- hub = Hub.current
17
- if hub is not None and hub.client is not None:
18
- return hub.client.options["debug"]
19
- return False
16
+
17
+ return get_client().options["debug"]
20
18
 
21
19
 
22
20
  def init_debug_support():
23
21
  # type: () -> None
24
22
  if not logger.handlers:
25
23
  configure_logger()
26
- configure_debug_hub()
27
24
 
28
25
 
29
26
  def configure_logger():
@@ -32,13 +29,13 @@ def configure_logger():
32
29
  _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s"))
33
30
  logger.addHandler(_handler)
34
31
  logger.setLevel(logging.DEBUG)
35
- logger.addFilter(_HubBasedClientFilter())
32
+ logger.addFilter(_DebugFilter())
36
33
 
37
34
 
38
35
  def configure_debug_hub():
39
36
  # type: () -> None
40
- def _get_debug_hub():
41
- # type: () -> Hub
42
- return Hub.current
43
-
44
- utils._get_debug_hub = _get_debug_hub
37
+ warnings.warn(
38
+ "configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.",
39
+ DeprecationWarning,
40
+ stacklevel=2,
41
+ )
sentry_sdk/envelope.py CHANGED
@@ -1,17 +1,14 @@
1
1
  import io
2
2
  import json
3
- import shutil
4
3
  import mimetypes
5
4
 
6
- from sentry_sdk._compat import text_type
7
- from sentry_sdk._types import MYPY
8
- from sentry_sdk.sessions import Session
9
- from sentry_sdk.tracing import Transaction
10
- from sentry_sdk.utils import json_dumps
5
+ from sentry_sdk.session import Session
6
+ from sentry_sdk.utils import json_dumps, capture_internal_exceptions
11
7
 
12
- if MYPY:
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
13
11
  from typing import Any
14
- from typing import Tuple
15
12
  from typing import Optional
16
13
  from typing import Union
17
14
  from typing import Dict
@@ -21,10 +18,24 @@ if MYPY:
21
18
  from sentry_sdk._types import Event, EventDataCategory
22
19
 
23
20
 
24
- class Envelope(object):
21
+ def parse_json(data):
22
+ # type: (Union[bytes, str]) -> Any
23
+ # on some python 3 versions this needs to be bytes
24
+ if isinstance(data, bytes):
25
+ data = data.decode("utf-8", "replace")
26
+ return json.loads(data)
27
+
28
+
29
+ class Envelope:
30
+ """
31
+ Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints
32
+ documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular,
33
+ each envelope may have at most one Item with type "event" or "transaction" (but not both).
34
+ """
35
+
25
36
  def __init__(
26
37
  self,
27
- headers=None, # type: Optional[Dict[str, str]]
38
+ headers=None, # type: Optional[Dict[str, Any]]
28
39
  items=None, # type: Optional[List[Item]]
29
40
  ):
30
41
  # type: (...) -> None
@@ -46,27 +57,65 @@ class Envelope(object):
46
57
  )
47
58
 
48
59
  def add_event(
49
- self, event # type: Event
60
+ self,
61
+ event, # type: Event
50
62
  ):
51
63
  # type: (...) -> None
52
64
  self.add_item(Item(payload=PayloadRef(json=event), type="event"))
53
65
 
54
66
  def add_transaction(
55
- self, transaction # type: Transaction
67
+ self,
68
+ transaction, # type: Event
56
69
  ):
57
70
  # type: (...) -> None
58
71
  self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))
59
72
 
73
+ def add_profile(
74
+ self,
75
+ profile, # type: Any
76
+ ):
77
+ # type: (...) -> None
78
+ self.add_item(Item(payload=PayloadRef(json=profile), type="profile"))
79
+
80
+ def add_profile_chunk(
81
+ self,
82
+ profile_chunk, # type: Any
83
+ ):
84
+ # type: (...) -> None
85
+ self.add_item(
86
+ Item(
87
+ payload=PayloadRef(json=profile_chunk),
88
+ type="profile_chunk",
89
+ headers={"platform": profile_chunk.get("platform", "python")},
90
+ )
91
+ )
92
+
93
+ def add_checkin(
94
+ self,
95
+ checkin, # type: Any
96
+ ):
97
+ # type: (...) -> None
98
+ self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in"))
99
+
60
100
  def add_session(
61
- self, session # type: Union[Session, Any]
101
+ self,
102
+ session, # type: Union[Session, Any]
62
103
  ):
63
104
  # type: (...) -> None
64
105
  if isinstance(session, Session):
65
106
  session = session.to_json()
66
107
  self.add_item(Item(payload=PayloadRef(json=session), type="session"))
67
108
 
109
+ def add_sessions(
110
+ self,
111
+ sessions, # type: Any
112
+ ):
113
+ # type: (...) -> None
114
+ self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
115
+
68
116
  def add_item(
69
- self, item # type: Item
117
+ self,
118
+ item, # type: Item
70
119
  ):
71
120
  # type: (...) -> None
72
121
  self.items.append(item)
@@ -92,7 +141,8 @@ class Envelope(object):
92
141
  return iter(self.items)
93
142
 
94
143
  def serialize_into(
95
- self, f # type: Any
144
+ self,
145
+ f, # type: Any
96
146
  ):
97
147
  # type: (...) -> None
98
148
  f.write(json_dumps(self.headers))
@@ -108,10 +158,11 @@ class Envelope(object):
108
158
 
109
159
  @classmethod
110
160
  def deserialize_from(
111
- cls, f # type: Any
161
+ cls,
162
+ f, # type: Any
112
163
  ):
113
164
  # type: (...) -> Envelope
114
- headers = json.loads(f.readline())
165
+ headers = parse_json(f.readline())
115
166
  items = []
116
167
  while 1:
117
168
  item = Item.deserialize_from(f)
@@ -122,7 +173,8 @@ class Envelope(object):
122
173
 
123
174
  @classmethod
124
175
  def deserialize(
125
- cls, bytes # type: bytes
176
+ cls,
177
+ bytes, # type: bytes
126
178
  ):
127
179
  # type: (...) -> Envelope
128
180
  return cls.deserialize_from(io.BytesIO(bytes))
@@ -132,11 +184,11 @@ class Envelope(object):
132
184
  return "<Envelope headers=%r items=%r>" % (self.headers, self.items)
133
185
 
134
186
 
135
- class PayloadRef(object):
187
+ class PayloadRef:
136
188
  def __init__(
137
189
  self,
138
190
  bytes=None, # type: Optional[bytes]
139
- path=None, # type: Optional[Union[bytes, text_type]]
191
+ path=None, # type: Optional[Union[bytes, str]]
140
192
  json=None, # type: Optional[Any]
141
193
  ):
142
194
  # type: (...) -> None
@@ -148,33 +200,12 @@ class PayloadRef(object):
148
200
  # type: (...) -> bytes
149
201
  if self.bytes is None:
150
202
  if self.path is not None:
151
- with open(self.path, "rb") as f:
152
- self.bytes = f.read()
203
+ with capture_internal_exceptions():
204
+ with open(self.path, "rb") as f:
205
+ self.bytes = f.read()
153
206
  elif self.json is not None:
154
207
  self.bytes = json_dumps(self.json)
155
- else:
156
- self.bytes = b""
157
- return self.bytes
158
-
159
- def _prepare_serialize(self):
160
- # type: (...) -> Tuple[Any, Any]
161
- if self.path is not None and self.bytes is None:
162
- f = open(self.path, "rb")
163
- f.seek(0, 2)
164
- length = f.tell()
165
- f.seek(0, 0)
166
-
167
- def writer(out):
168
- # type: (Any) -> None
169
- try:
170
- shutil.copyfileobj(f, out)
171
- finally:
172
- f.close()
173
-
174
- return length, writer
175
-
176
- bytes = self.get_bytes()
177
- return len(bytes), lambda f: f.write(bytes)
208
+ return self.bytes or b""
178
209
 
179
210
  @property
180
211
  def inferred_content_type(self):
@@ -195,11 +226,11 @@ class PayloadRef(object):
195
226
  return "<Payload %r>" % (self.inferred_content_type,)
196
227
 
197
228
 
198
- class Item(object):
229
+ class Item:
199
230
  def __init__(
200
231
  self,
201
- payload, # type: Union[bytes, text_type, PayloadRef]
202
- headers=None, # type: Optional[Dict[str, str]]
232
+ payload, # type: Union[bytes, str, PayloadRef]
233
+ headers=None, # type: Optional[Dict[str, Any]]
203
234
  type=None, # type: Optional[str]
204
235
  content_type=None, # type: Optional[str]
205
236
  filename=None, # type: Optional[str]
@@ -211,7 +242,7 @@ class Item(object):
211
242
  self.headers = headers
212
243
  if isinstance(payload, bytes):
213
244
  payload = PayloadRef(bytes=payload)
214
- elif isinstance(payload, text_type):
245
+ elif isinstance(payload, str):
215
246
  payload = PayloadRef(bytes=payload.encode("utf-8"))
216
247
  else:
217
248
  payload = payload
@@ -244,7 +275,7 @@ class Item(object):
244
275
  def data_category(self):
245
276
  # type: (...) -> EventDataCategory
246
277
  ty = self.headers.get("type")
247
- if ty == "session":
278
+ if ty == "session" or ty == "sessions":
248
279
  return "session"
249
280
  elif ty == "attachment":
250
281
  return "attachment"
@@ -252,6 +283,18 @@ class Item(object):
252
283
  return "transaction"
253
284
  elif ty == "event":
254
285
  return "error"
286
+ elif ty == "log":
287
+ return "log_item"
288
+ elif ty == "trace_metric":
289
+ return "trace_metric"
290
+ elif ty == "client_report":
291
+ return "internal"
292
+ elif ty == "profile":
293
+ return "profile"
294
+ elif ty == "profile_chunk":
295
+ return "profile_chunk"
296
+ elif ty == "check_in":
297
+ return "monitor"
255
298
  else:
256
299
  return "default"
257
300
 
@@ -275,15 +318,16 @@ class Item(object):
275
318
  return None
276
319
 
277
320
  def serialize_into(
278
- self, f # type: Any
321
+ self,
322
+ f, # type: Any
279
323
  ):
280
324
  # type: (...) -> None
281
325
  headers = dict(self.headers)
282
- length, writer = self.payload._prepare_serialize()
283
- headers["length"] = length
326
+ bytes = self.get_bytes()
327
+ headers["length"] = len(bytes)
284
328
  f.write(json_dumps(headers))
285
329
  f.write(b"\n")
286
- writer(f)
330
+ f.write(bytes)
287
331
  f.write(b"\n")
288
332
 
289
333
  def serialize(self):
@@ -294,25 +338,32 @@ class Item(object):
294
338
 
295
339
  @classmethod
296
340
  def deserialize_from(
297
- cls, f # type: Any
341
+ cls,
342
+ f, # type: Any
298
343
  ):
299
344
  # type: (...) -> Optional[Item]
300
345
  line = f.readline().rstrip()
301
346
  if not line:
302
347
  return None
303
- headers = json.loads(line)
304
- length = headers["length"]
305
- payload = f.read(length)
348
+ headers = parse_json(line)
349
+ length = headers.get("length")
350
+ if length is not None:
351
+ payload = f.read(length)
352
+ f.readline()
353
+ else:
354
+ # if no length was specified we need to read up to the end of line
355
+ # and remove it (if it is present, i.e. not the very last char in an eof terminated envelope)
356
+ payload = f.readline().rstrip(b"\n")
306
357
  if headers.get("type") in ("event", "transaction"):
307
- rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload)))
358
+ rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
308
359
  else:
309
360
  rv = cls(headers=headers, payload=payload)
310
- f.readline()
311
361
  return rv
312
362
 
313
363
  @classmethod
314
364
  def deserialize(
315
- cls, bytes # type: bytes
365
+ cls,
366
+ bytes, # type: bytes
316
367
  ):
317
368
  # type: (...) -> Optional[Item]
318
369
  return cls.deserialize_from(io.BytesIO(bytes))
@@ -0,0 +1,71 @@
1
+ import copy
2
+ import sentry_sdk
3
+ from sentry_sdk._lru_cache import LRUCache
4
+ from threading import Lock
5
+
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import TypedDict
10
+
11
+ FlagData = TypedDict("FlagData", {"flag": str, "result": bool})
12
+
13
+
14
+ DEFAULT_FLAG_CAPACITY = 100
15
+
16
+
17
+ class FlagBuffer:
18
+ def __init__(self, capacity):
19
+ # type: (int) -> None
20
+ self.capacity = capacity
21
+ self.lock = Lock()
22
+
23
+ # Buffer is private. The name is mangled to discourage use. If you use this attribute
24
+ # directly you're on your own!
25
+ self.__buffer = LRUCache(capacity)
26
+
27
+ def clear(self):
28
+ # type: () -> None
29
+ self.__buffer = LRUCache(self.capacity)
30
+
31
+ def __deepcopy__(self, memo):
32
+ # type: (dict[int, Any]) -> FlagBuffer
33
+ with self.lock:
34
+ buffer = FlagBuffer(self.capacity)
35
+ buffer.__buffer = copy.deepcopy(self.__buffer, memo)
36
+ return buffer
37
+
38
+ def get(self):
39
+ # type: () -> list[FlagData]
40
+ with self.lock:
41
+ return [
42
+ {"flag": key, "result": value} for key, value in self.__buffer.get_all()
43
+ ]
44
+
45
+ def set(self, flag, result):
46
+ # type: (str, bool) -> None
47
+ if isinstance(result, FlagBuffer):
48
+ # If someone were to insert `self` into `self` this would create a circular dependency
49
+ # on the lock. This is of course a deadlock. However, this is far outside the expected
50
+ # usage of this class. We guard against it here for completeness and to document this
51
+ # expected failure mode.
52
+ raise ValueError(
53
+ "FlagBuffer instances can not be inserted into the dictionary."
54
+ )
55
+
56
+ with self.lock:
57
+ self.__buffer.set(flag, result)
58
+
59
+
60
+ def add_feature_flag(flag, result):
61
+ # type: (str, bool) -> None
62
+ """
63
+ Records a flag and its value to be sent on subsequent error events.
64
+ We recommend you do this on flag evaluations. Flags are buffered per Sentry scope.
65
+ """
66
+ flags = sentry_sdk.get_isolation_scope().flags
67
+ flags.set(flag, result)
68
+
69
+ span = sentry_sdk.get_current_span()
70
+ if span:
71
+ span.set_flag(f"flag.evaluation.{flag}", result)