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,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,40 +1,41 @@
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():
21
+ # type: () -> None
23
22
  if not logger.handlers:
24
23
  configure_logger()
25
- configure_debug_hub()
26
24
 
27
25
 
28
26
  def configure_logger():
27
+ # type: () -> None
29
28
  _handler = logging.StreamHandler(sys.stderr)
30
29
  _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s"))
31
30
  logger.addHandler(_handler)
32
31
  logger.setLevel(logging.DEBUG)
33
- logger.addFilter(_HubBasedClientFilter())
32
+ logger.addFilter(_DebugFilter())
34
33
 
35
34
 
36
35
  def configure_debug_hub():
37
- def _get_debug_hub():
38
- return Hub.current
39
-
40
- utils._get_debug_hub = _get_debug_hub
36
+ # type: () -> None
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 ADDED
@@ -0,0 +1,369 @@
1
+ import io
2
+ import json
3
+ import mimetypes
4
+
5
+ from sentry_sdk.session import Session
6
+ from sentry_sdk.utils import json_dumps, capture_internal_exceptions
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Any
12
+ from typing import Optional
13
+ from typing import Union
14
+ from typing import Dict
15
+ from typing import List
16
+ from typing import Iterator
17
+
18
+ from sentry_sdk._types import Event, EventDataCategory
19
+
20
+
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
+
36
+ def __init__(
37
+ self,
38
+ headers=None, # type: Optional[Dict[str, Any]]
39
+ items=None, # type: Optional[List[Item]]
40
+ ):
41
+ # type: (...) -> None
42
+ if headers is not None:
43
+ headers = dict(headers)
44
+ self.headers = headers or {}
45
+ if items is None:
46
+ items = []
47
+ else:
48
+ items = list(items)
49
+ self.items = items
50
+
51
+ @property
52
+ def description(self):
53
+ # type: (...) -> str
54
+ return "envelope with %s items (%s)" % (
55
+ len(self.items),
56
+ ", ".join(x.data_category for x in self.items),
57
+ )
58
+
59
+ def add_event(
60
+ self,
61
+ event, # type: Event
62
+ ):
63
+ # type: (...) -> None
64
+ self.add_item(Item(payload=PayloadRef(json=event), type="event"))
65
+
66
+ def add_transaction(
67
+ self,
68
+ transaction, # type: Event
69
+ ):
70
+ # type: (...) -> None
71
+ self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))
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
+
100
+ def add_session(
101
+ self,
102
+ session, # type: Union[Session, Any]
103
+ ):
104
+ # type: (...) -> None
105
+ if isinstance(session, Session):
106
+ session = session.to_json()
107
+ self.add_item(Item(payload=PayloadRef(json=session), type="session"))
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
+
116
+ def add_item(
117
+ self,
118
+ item, # type: Item
119
+ ):
120
+ # type: (...) -> None
121
+ self.items.append(item)
122
+
123
+ def get_event(self):
124
+ # type: (...) -> Optional[Event]
125
+ for items in self.items:
126
+ event = items.get_event()
127
+ if event is not None:
128
+ return event
129
+ return None
130
+
131
+ def get_transaction_event(self):
132
+ # type: (...) -> Optional[Event]
133
+ for item in self.items:
134
+ event = item.get_transaction_event()
135
+ if event is not None:
136
+ return event
137
+ return None
138
+
139
+ def __iter__(self):
140
+ # type: (...) -> Iterator[Item]
141
+ return iter(self.items)
142
+
143
+ def serialize_into(
144
+ self,
145
+ f, # type: Any
146
+ ):
147
+ # type: (...) -> None
148
+ f.write(json_dumps(self.headers))
149
+ f.write(b"\n")
150
+ for item in self.items:
151
+ item.serialize_into(f)
152
+
153
+ def serialize(self):
154
+ # type: (...) -> bytes
155
+ out = io.BytesIO()
156
+ self.serialize_into(out)
157
+ return out.getvalue()
158
+
159
+ @classmethod
160
+ def deserialize_from(
161
+ cls,
162
+ f, # type: Any
163
+ ):
164
+ # type: (...) -> Envelope
165
+ headers = parse_json(f.readline())
166
+ items = []
167
+ while 1:
168
+ item = Item.deserialize_from(f)
169
+ if item is None:
170
+ break
171
+ items.append(item)
172
+ return cls(headers=headers, items=items)
173
+
174
+ @classmethod
175
+ def deserialize(
176
+ cls,
177
+ bytes, # type: bytes
178
+ ):
179
+ # type: (...) -> Envelope
180
+ return cls.deserialize_from(io.BytesIO(bytes))
181
+
182
+ def __repr__(self):
183
+ # type: (...) -> str
184
+ return "<Envelope headers=%r items=%r>" % (self.headers, self.items)
185
+
186
+
187
+ class PayloadRef:
188
+ def __init__(
189
+ self,
190
+ bytes=None, # type: Optional[bytes]
191
+ path=None, # type: Optional[Union[bytes, str]]
192
+ json=None, # type: Optional[Any]
193
+ ):
194
+ # type: (...) -> None
195
+ self.json = json
196
+ self.bytes = bytes
197
+ self.path = path
198
+
199
+ def get_bytes(self):
200
+ # type: (...) -> bytes
201
+ if self.bytes is None:
202
+ if self.path is not None:
203
+ with capture_internal_exceptions():
204
+ with open(self.path, "rb") as f:
205
+ self.bytes = f.read()
206
+ elif self.json is not None:
207
+ self.bytes = json_dumps(self.json)
208
+ return self.bytes or b""
209
+
210
+ @property
211
+ def inferred_content_type(self):
212
+ # type: (...) -> str
213
+ if self.json is not None:
214
+ return "application/json"
215
+ elif self.path is not None:
216
+ path = self.path
217
+ if isinstance(path, bytes):
218
+ path = path.decode("utf-8", "replace")
219
+ ty = mimetypes.guess_type(path)[0]
220
+ if ty:
221
+ return ty
222
+ return "application/octet-stream"
223
+
224
+ def __repr__(self):
225
+ # type: (...) -> str
226
+ return "<Payload %r>" % (self.inferred_content_type,)
227
+
228
+
229
+ class Item:
230
+ def __init__(
231
+ self,
232
+ payload, # type: Union[bytes, str, PayloadRef]
233
+ headers=None, # type: Optional[Dict[str, Any]]
234
+ type=None, # type: Optional[str]
235
+ content_type=None, # type: Optional[str]
236
+ filename=None, # type: Optional[str]
237
+ ):
238
+ if headers is not None:
239
+ headers = dict(headers)
240
+ elif headers is None:
241
+ headers = {}
242
+ self.headers = headers
243
+ if isinstance(payload, bytes):
244
+ payload = PayloadRef(bytes=payload)
245
+ elif isinstance(payload, str):
246
+ payload = PayloadRef(bytes=payload.encode("utf-8"))
247
+ else:
248
+ payload = payload
249
+
250
+ if filename is not None:
251
+ headers["filename"] = filename
252
+ if type is not None:
253
+ headers["type"] = type
254
+ if content_type is not None:
255
+ headers["content_type"] = content_type
256
+ elif "content_type" not in headers:
257
+ headers["content_type"] = payload.inferred_content_type
258
+
259
+ self.payload = payload
260
+
261
+ def __repr__(self):
262
+ # type: (...) -> str
263
+ return "<Item headers=%r payload=%r data_category=%r>" % (
264
+ self.headers,
265
+ self.payload,
266
+ self.data_category,
267
+ )
268
+
269
+ @property
270
+ def type(self):
271
+ # type: (...) -> Optional[str]
272
+ return self.headers.get("type")
273
+
274
+ @property
275
+ def data_category(self):
276
+ # type: (...) -> EventDataCategory
277
+ ty = self.headers.get("type")
278
+ if ty == "session" or ty == "sessions":
279
+ return "session"
280
+ elif ty == "attachment":
281
+ return "attachment"
282
+ elif ty == "transaction":
283
+ return "transaction"
284
+ elif ty == "event":
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"
298
+ else:
299
+ return "default"
300
+
301
+ def get_bytes(self):
302
+ # type: (...) -> bytes
303
+ return self.payload.get_bytes()
304
+
305
+ def get_event(self):
306
+ # type: (...) -> Optional[Event]
307
+ """
308
+ Returns an error event if there is one.
309
+ """
310
+ if self.type == "event" and self.payload.json is not None:
311
+ return self.payload.json
312
+ return None
313
+
314
+ def get_transaction_event(self):
315
+ # type: (...) -> Optional[Event]
316
+ if self.type == "transaction" and self.payload.json is not None:
317
+ return self.payload.json
318
+ return None
319
+
320
+ def serialize_into(
321
+ self,
322
+ f, # type: Any
323
+ ):
324
+ # type: (...) -> None
325
+ headers = dict(self.headers)
326
+ bytes = self.get_bytes()
327
+ headers["length"] = len(bytes)
328
+ f.write(json_dumps(headers))
329
+ f.write(b"\n")
330
+ f.write(bytes)
331
+ f.write(b"\n")
332
+
333
+ def serialize(self):
334
+ # type: (...) -> bytes
335
+ out = io.BytesIO()
336
+ self.serialize_into(out)
337
+ return out.getvalue()
338
+
339
+ @classmethod
340
+ def deserialize_from(
341
+ cls,
342
+ f, # type: Any
343
+ ):
344
+ # type: (...) -> Optional[Item]
345
+ line = f.readline().rstrip()
346
+ if not line:
347
+ return None
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")
357
+ if headers.get("type") in ("event", "transaction"):
358
+ rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
359
+ else:
360
+ rv = cls(headers=headers, payload=payload)
361
+ return rv
362
+
363
+ @classmethod
364
+ def deserialize(
365
+ cls,
366
+ bytes, # type: bytes
367
+ ):
368
+ # type: (...) -> Optional[Item]
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)