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
sentry_sdk/session.py ADDED
@@ -0,0 +1,177 @@
1
+ import uuid
2
+ from datetime import datetime, timezone
3
+
4
+ from sentry_sdk.utils import format_timestamp
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import Optional
10
+ from typing import Union
11
+ from typing import Any
12
+ from typing import Dict
13
+
14
+ from sentry_sdk._types import SessionStatus
15
+
16
+
17
+ def _minute_trunc(ts):
18
+ # type: (datetime) -> datetime
19
+ return ts.replace(second=0, microsecond=0)
20
+
21
+
22
+ def _make_uuid(
23
+ val, # type: Union[str, uuid.UUID]
24
+ ):
25
+ # type: (...) -> uuid.UUID
26
+ if isinstance(val, uuid.UUID):
27
+ return val
28
+ return uuid.UUID(val)
29
+
30
+
31
+ class Session:
32
+ def __init__(
33
+ self,
34
+ sid=None, # type: Optional[Union[str, uuid.UUID]]
35
+ did=None, # type: Optional[str]
36
+ timestamp=None, # type: Optional[datetime]
37
+ started=None, # type: Optional[datetime]
38
+ duration=None, # type: Optional[float]
39
+ status=None, # type: Optional[SessionStatus]
40
+ release=None, # type: Optional[str]
41
+ environment=None, # type: Optional[str]
42
+ user_agent=None, # type: Optional[str]
43
+ ip_address=None, # type: Optional[str]
44
+ errors=None, # type: Optional[int]
45
+ user=None, # type: Optional[Any]
46
+ session_mode="application", # type: str
47
+ ):
48
+ # type: (...) -> None
49
+ if sid is None:
50
+ sid = uuid.uuid4()
51
+ if started is None:
52
+ started = datetime.now(timezone.utc)
53
+ if status is None:
54
+ status = "ok"
55
+ self.status = status
56
+ self.did = None # type: Optional[str]
57
+ self.started = started
58
+ self.release = None # type: Optional[str]
59
+ self.environment = None # type: Optional[str]
60
+ self.duration = None # type: Optional[float]
61
+ self.user_agent = None # type: Optional[str]
62
+ self.ip_address = None # type: Optional[str]
63
+ self.session_mode = session_mode # type: str
64
+ self.errors = 0
65
+
66
+ self.update(
67
+ sid=sid,
68
+ did=did,
69
+ timestamp=timestamp,
70
+ duration=duration,
71
+ release=release,
72
+ environment=environment,
73
+ user_agent=user_agent,
74
+ ip_address=ip_address,
75
+ errors=errors,
76
+ user=user,
77
+ )
78
+
79
+ @property
80
+ def truncated_started(self):
81
+ # type: (...) -> datetime
82
+ return _minute_trunc(self.started)
83
+
84
+ def update(
85
+ self,
86
+ sid=None, # type: Optional[Union[str, uuid.UUID]]
87
+ did=None, # type: Optional[str]
88
+ timestamp=None, # type: Optional[datetime]
89
+ started=None, # type: Optional[datetime]
90
+ duration=None, # type: Optional[float]
91
+ status=None, # type: Optional[SessionStatus]
92
+ release=None, # type: Optional[str]
93
+ environment=None, # type: Optional[str]
94
+ user_agent=None, # type: Optional[str]
95
+ ip_address=None, # type: Optional[str]
96
+ errors=None, # type: Optional[int]
97
+ user=None, # type: Optional[Any]
98
+ ):
99
+ # type: (...) -> None
100
+ # If a user is supplied we pull some data form it
101
+ if user:
102
+ if ip_address is None:
103
+ ip_address = user.get("ip_address")
104
+ if did is None:
105
+ did = user.get("id") or user.get("email") or user.get("username")
106
+
107
+ if sid is not None:
108
+ self.sid = _make_uuid(sid)
109
+ if did is not None:
110
+ self.did = str(did)
111
+ if timestamp is None:
112
+ timestamp = datetime.now(timezone.utc)
113
+ self.timestamp = timestamp
114
+ if started is not None:
115
+ self.started = started
116
+ if duration is not None:
117
+ self.duration = duration
118
+ if release is not None:
119
+ self.release = release
120
+ if environment is not None:
121
+ self.environment = environment
122
+ if ip_address is not None:
123
+ self.ip_address = ip_address
124
+ if user_agent is not None:
125
+ self.user_agent = user_agent
126
+ if errors is not None:
127
+ self.errors = errors
128
+
129
+ if status is not None:
130
+ self.status = status
131
+
132
+ def close(
133
+ self,
134
+ status=None, # type: Optional[SessionStatus]
135
+ ):
136
+ # type: (...) -> Any
137
+ if status is None and self.status == "ok":
138
+ status = "exited"
139
+ if status is not None:
140
+ self.update(status=status)
141
+
142
+ def get_json_attrs(
143
+ self,
144
+ with_user_info=True, # type: Optional[bool]
145
+ ):
146
+ # type: (...) -> Any
147
+ attrs = {}
148
+ if self.release is not None:
149
+ attrs["release"] = self.release
150
+ if self.environment is not None:
151
+ attrs["environment"] = self.environment
152
+ if with_user_info:
153
+ if self.ip_address is not None:
154
+ attrs["ip_address"] = self.ip_address
155
+ if self.user_agent is not None:
156
+ attrs["user_agent"] = self.user_agent
157
+ return attrs
158
+
159
+ def to_json(self):
160
+ # type: (...) -> Any
161
+ rv = {
162
+ "sid": str(self.sid),
163
+ "init": True,
164
+ "started": format_timestamp(self.started),
165
+ "timestamp": format_timestamp(self.timestamp),
166
+ "status": self.status,
167
+ } # type: Dict[str, Any]
168
+ if self.errors:
169
+ rv["errors"] = self.errors
170
+ if self.did is not None:
171
+ rv["did"] = self.did
172
+ if self.duration is not None:
173
+ rv["duration"] = self.duration
174
+ attrs = self.get_json_attrs()
175
+ if attrs:
176
+ rv["attrs"] = attrs
177
+ return rv
sentry_sdk/sessions.py CHANGED
@@ -1,46 +1,69 @@
1
1
  import os
2
- import uuid
3
- import time
4
- from datetime import datetime
5
- from threading import Thread, Lock
2
+ import warnings
3
+ from threading import Thread, Lock, Event
6
4
  from contextlib import contextmanager
7
5
 
8
- from sentry_sdk._types import MYPY
6
+ import sentry_sdk
7
+ from sentry_sdk.envelope import Envelope
8
+ from sentry_sdk.session import Session
9
9
  from sentry_sdk.utils import format_timestamp
10
10
 
11
- if MYPY:
12
- import sentry_sdk
11
+ from typing import TYPE_CHECKING
13
12
 
14
- from typing import Optional
15
- from typing import Union
13
+ if TYPE_CHECKING:
16
14
  from typing import Any
15
+ from typing import Callable
17
16
  from typing import Dict
18
17
  from typing import Generator
19
-
20
- from sentry_sdk._types import SessionStatus
18
+ from typing import List
19
+ from typing import Optional
20
+ from typing import Union
21
21
 
22
22
 
23
23
  def is_auto_session_tracking_enabled(hub=None):
24
- # type: (Optional[sentry_sdk.Hub]) -> bool
25
- """Utility function to find out if session tracking is enabled."""
24
+ # type: (Optional[sentry_sdk.Hub]) -> Union[Any, bool, None]
25
+ """DEPRECATED: Utility function to find out if session tracking is enabled."""
26
+
27
+ # Internal callers should use private _is_auto_session_tracking_enabled, instead.
28
+ warnings.warn(
29
+ "This function is deprecated and will be removed in the next major release. "
30
+ "There is no public API replacement.",
31
+ DeprecationWarning,
32
+ stacklevel=2,
33
+ )
34
+
26
35
  if hub is None:
27
36
  hub = sentry_sdk.Hub.current
37
+
28
38
  should_track = hub.scope._force_auto_session_tracking
39
+
29
40
  if should_track is None:
30
- exp = hub.client.options["_experiments"] if hub.client else {}
31
- should_track = exp.get("auto_session_tracking")
41
+ client_options = hub.client.options if hub.client else {}
42
+ should_track = client_options.get("auto_session_tracking", False)
43
+
32
44
  return should_track
33
45
 
34
46
 
35
47
  @contextmanager
36
- def auto_session_tracking(hub=None):
37
- # type: (Optional[sentry_sdk.Hub]) -> Generator[None, None, None]
38
- """Starts and stops a session automatically around a block."""
48
+ def auto_session_tracking(hub=None, session_mode="application"):
49
+ # type: (Optional[sentry_sdk.Hub], str) -> Generator[None, None, None]
50
+ """DEPRECATED: Use track_session instead
51
+ Starts and stops a session automatically around a block.
52
+ """
53
+ warnings.warn(
54
+ "This function is deprecated and will be removed in the next major release. "
55
+ "Use track_session instead.",
56
+ DeprecationWarning,
57
+ stacklevel=2,
58
+ )
59
+
39
60
  if hub is None:
40
61
  hub = sentry_sdk.Hub.current
41
- should_track = is_auto_session_tracking_enabled(hub)
62
+ with warnings.catch_warnings():
63
+ warnings.simplefilter("ignore", DeprecationWarning)
64
+ should_track = is_auto_session_tracking_enabled(hub)
42
65
  if should_track:
43
- hub.start_session()
66
+ hub.start_session(session_mode=session_mode)
44
67
  try:
45
68
  yield
46
69
  finally:
@@ -48,41 +71,134 @@ def auto_session_tracking(hub=None):
48
71
  hub.end_session()
49
72
 
50
73
 
51
- def _make_uuid(
52
- val, # type: Union[str, uuid.UUID]
53
- ):
54
- # type: (...) -> uuid.UUID
55
- if isinstance(val, uuid.UUID):
56
- return val
57
- return uuid.UUID(val)
74
+ def is_auto_session_tracking_enabled_scope(scope):
75
+ # type: (sentry_sdk.Scope) -> bool
76
+ """
77
+ DEPRECATED: Utility function to find out if session tracking is enabled.
78
+ """
79
+
80
+ warnings.warn(
81
+ "This function is deprecated and will be removed in the next major release. "
82
+ "There is no public API replacement.",
83
+ DeprecationWarning,
84
+ stacklevel=2,
85
+ )
86
+
87
+ # Internal callers should use private _is_auto_session_tracking_enabled, instead.
88
+ return _is_auto_session_tracking_enabled(scope)
89
+
90
+
91
+ def _is_auto_session_tracking_enabled(scope):
92
+ # type: (sentry_sdk.Scope) -> bool
93
+ """
94
+ Utility function to find out if session tracking is enabled.
95
+ """
96
+
97
+ should_track = scope._force_auto_session_tracking
98
+ if should_track is None:
99
+ client_options = sentry_sdk.get_client().options
100
+ should_track = client_options.get("auto_session_tracking", False)
101
+
102
+ return should_track
103
+
104
+
105
+ @contextmanager
106
+ def auto_session_tracking_scope(scope, session_mode="application"):
107
+ # type: (sentry_sdk.Scope, str) -> Generator[None, None, None]
108
+ """DEPRECATED: This function is a deprecated alias for track_session.
109
+ Starts and stops a session automatically around a block.
110
+ """
111
+
112
+ warnings.warn(
113
+ "This function is a deprecated alias for track_session and will be removed in the next major release.",
114
+ DeprecationWarning,
115
+ stacklevel=2,
116
+ )
117
+
118
+ with track_session(scope, session_mode=session_mode):
119
+ yield
120
+
121
+
122
+ @contextmanager
123
+ def track_session(scope, session_mode="application"):
124
+ # type: (sentry_sdk.Scope, str) -> Generator[None, None, None]
125
+ """
126
+ Start a new session in the provided scope, assuming session tracking is enabled.
127
+ This is a no-op context manager if session tracking is not enabled.
128
+ """
129
+
130
+ should_track = _is_auto_session_tracking_enabled(scope)
131
+ if should_track:
132
+ scope.start_session(session_mode=session_mode)
133
+ try:
134
+ yield
135
+ finally:
136
+ if should_track:
137
+ scope.end_session()
58
138
 
59
139
 
60
140
  TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed")
141
+ MAX_ENVELOPE_ITEMS = 100
142
+
61
143
 
144
+ def make_aggregate_envelope(aggregate_states, attrs):
145
+ # type: (Any, Any) -> Any
146
+ return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())}
62
147
 
63
- class SessionFlusher(object):
148
+
149
+ class SessionFlusher:
64
150
  def __init__(
65
151
  self,
66
- flush_func, # type: Any
67
- flush_interval=10, # type: int
152
+ capture_func, # type: Callable[[Envelope], None]
153
+ flush_interval=60, # type: int
68
154
  ):
69
155
  # type: (...) -> None
70
- self.flush_func = flush_func
156
+ self.capture_func = capture_func
71
157
  self.flush_interval = flush_interval
72
- self.pending = {} # type: Dict[str, Any]
158
+ self.pending_sessions = [] # type: List[Any]
159
+ self.pending_aggregates = {} # type: Dict[Any, Any]
73
160
  self._thread = None # type: Optional[Thread]
74
161
  self._thread_lock = Lock()
162
+ self._aggregate_lock = Lock()
75
163
  self._thread_for_pid = None # type: Optional[int]
76
- self._running = True
164
+ self.__shutdown_requested = Event()
77
165
 
78
166
  def flush(self):
79
167
  # type: (...) -> None
80
- pending = self.pending
81
- self.pending = {}
82
- self.flush_func(list(pending.values()))
168
+ pending_sessions = self.pending_sessions
169
+ self.pending_sessions = []
170
+
171
+ with self._aggregate_lock:
172
+ pending_aggregates = self.pending_aggregates
173
+ self.pending_aggregates = {}
174
+
175
+ envelope = Envelope()
176
+ for session in pending_sessions:
177
+ if len(envelope.items) == MAX_ENVELOPE_ITEMS:
178
+ self.capture_func(envelope)
179
+ envelope = Envelope()
180
+
181
+ envelope.add_session(session)
182
+
183
+ for attrs, states in pending_aggregates.items():
184
+ if len(envelope.items) == MAX_ENVELOPE_ITEMS:
185
+ self.capture_func(envelope)
186
+ envelope = Envelope()
187
+
188
+ envelope.add_sessions(make_aggregate_envelope(states, attrs))
189
+
190
+ if len(envelope.items) > 0:
191
+ self.capture_func(envelope)
83
192
 
84
193
  def _ensure_running(self):
85
194
  # type: (...) -> None
195
+ """
196
+ Check that we have an active thread to run in, or create one if not.
197
+
198
+ Note that this might fail (e.g. in Python 3.12 it's not possible to
199
+ spawn new threads at interpreter shutdown). In that case self._running
200
+ will be False after running this function.
201
+ """
86
202
  if self._thread_for_pid == os.getpid() and self._thread is not None:
87
203
  return None
88
204
  with self._thread_lock:
@@ -91,162 +207,69 @@ class SessionFlusher(object):
91
207
 
92
208
  def _thread():
93
209
  # type: (...) -> None
94
- while self._running:
95
- time.sleep(self.flush_interval)
96
- if self.pending and self._running:
97
- self.flush()
210
+ running = True
211
+ while running:
212
+ running = not self.__shutdown_requested.wait(self.flush_interval)
213
+ self.flush()
98
214
 
99
215
  thread = Thread(target=_thread)
100
216
  thread.daemon = True
101
- thread.start()
217
+ try:
218
+ thread.start()
219
+ except RuntimeError:
220
+ # Unfortunately at this point the interpreter is in a state that no
221
+ # longer allows us to spawn a thread and we have to bail.
222
+ self.__shutdown_requested.set()
223
+ return None
224
+
102
225
  self._thread = thread
103
226
  self._thread_for_pid = os.getpid()
227
+
104
228
  return None
105
229
 
106
- def add_session(
107
- self, session # type: Session
230
+ def add_aggregate_session(
231
+ self,
232
+ session, # type: Session
108
233
  ):
109
234
  # type: (...) -> None
110
- self.pending[session.sid.hex] = session.to_json()
111
- self._ensure_running()
112
-
113
- def kill(self):
114
- # type: (...) -> None
115
- self._running = False
235
+ # NOTE on `session.did`:
236
+ # the protocol can deal with buckets that have a distinct-id, however
237
+ # in practice we expect the python SDK to have an extremely high cardinality
238
+ # here, effectively making aggregation useless, therefore we do not
239
+ # aggregate per-did.
116
240
 
117
- def __del__(self):
118
- # type: (...) -> None
119
- self.kill()
241
+ # For this part we can get away with using the global interpreter lock
242
+ with self._aggregate_lock:
243
+ attrs = session.get_json_attrs(with_user_info=False)
244
+ primary_key = tuple(sorted(attrs.items()))
245
+ secondary_key = session.truncated_started # (, session.did)
246
+ states = self.pending_aggregates.setdefault(primary_key, {})
247
+ state = states.setdefault(secondary_key, {})
120
248
 
249
+ if "started" not in state:
250
+ state["started"] = format_timestamp(session.truncated_started)
251
+ # if session.did is not None:
252
+ # state["did"] = session.did
253
+ if session.status == "crashed":
254
+ state["crashed"] = state.get("crashed", 0) + 1
255
+ elif session.status == "abnormal":
256
+ state["abnormal"] = state.get("abnormal", 0) + 1
257
+ elif session.errors > 0:
258
+ state["errored"] = state.get("errored", 0) + 1
259
+ else:
260
+ state["exited"] = state.get("exited", 0) + 1
121
261
 
122
- class Session(object):
123
- def __init__(
262
+ def add_session(
124
263
  self,
125
- sid=None, # type: Optional[Union[str, uuid.UUID]]
126
- did=None, # type: Optional[str]
127
- timestamp=None, # type: Optional[datetime]
128
- started=None, # type: Optional[datetime]
129
- duration=None, # type: Optional[float]
130
- status=None, # type: Optional[SessionStatus]
131
- release=None, # type: Optional[str]
132
- environment=None, # type: Optional[str]
133
- user_agent=None, # type: Optional[str]
134
- ip_address=None, # type: Optional[str]
135
- errors=None, # type: Optional[int]
136
- user=None, # type: Optional[Any]
264
+ session, # type: Session
137
265
  ):
138
266
  # type: (...) -> None
139
- if sid is None:
140
- sid = uuid.uuid4()
141
- if started is None:
142
- started = datetime.utcnow()
143
- if status is None:
144
- status = "ok"
145
- self.status = status
146
- self.did = None # type: Optional[str]
147
- self.started = started
148
- self.release = None # type: Optional[str]
149
- self.environment = None # type: Optional[str]
150
- self.duration = None # type: Optional[float]
151
- self.user_agent = None # type: Optional[str]
152
- self.ip_address = None # type: Optional[str]
153
- self.errors = 0
154
-
155
- self.update(
156
- sid=sid,
157
- did=did,
158
- timestamp=timestamp,
159
- duration=duration,
160
- release=release,
161
- environment=environment,
162
- user_agent=user_agent,
163
- ip_address=ip_address,
164
- errors=errors,
165
- user=user,
166
- )
167
-
168
- def update(
169
- self,
170
- sid=None, # type: Optional[Union[str, uuid.UUID]]
171
- did=None, # type: Optional[str]
172
- timestamp=None, # type: Optional[datetime]
173
- started=None, # type: Optional[datetime]
174
- duration=None, # type: Optional[float]
175
- status=None, # type: Optional[SessionStatus]
176
- release=None, # type: Optional[str]
177
- environment=None, # type: Optional[str]
178
- user_agent=None, # type: Optional[str]
179
- ip_address=None, # type: Optional[str]
180
- errors=None, # type: Optional[int]
181
- user=None, # type: Optional[Any]
182
- ):
267
+ if session.session_mode == "request":
268
+ self.add_aggregate_session(session)
269
+ else:
270
+ self.pending_sessions.append(session.to_json())
271
+ self._ensure_running()
272
+
273
+ def kill(self):
183
274
  # type: (...) -> None
184
- # If a user is supplied we pull some data form it
185
- if user:
186
- if ip_address is None:
187
- ip_address = user.get("ip_address")
188
- if did is None:
189
- did = user.get("id") or user.get("email") or user.get("username")
190
-
191
- if sid is not None:
192
- self.sid = _make_uuid(sid)
193
- if did is not None:
194
- self.did = str(did)
195
- if timestamp is None:
196
- timestamp = datetime.utcnow()
197
- self.timestamp = timestamp
198
- if started is not None:
199
- self.started = started
200
- if duration is not None:
201
- self.duration = duration
202
- if release is not None:
203
- self.release = release
204
- if environment is not None:
205
- self.environment = environment
206
- if ip_address is not None:
207
- self.ip_address = ip_address
208
- if user_agent is not None:
209
- self.user_agent = user_agent
210
- if errors is not None:
211
- self.errors = errors
212
-
213
- if status is not None:
214
- self.status = status
215
-
216
- def close(
217
- self, status=None # type: Optional[SessionStatus]
218
- ):
219
- # type: (...) -> Any
220
- if status is None and self.status == "ok":
221
- status = "exited"
222
- if status is not None:
223
- self.update(status=status)
224
-
225
- def to_json(self):
226
- # type: (...) -> Any
227
- rv = {
228
- "sid": str(self.sid),
229
- "init": True,
230
- "started": format_timestamp(self.started),
231
- "timestamp": format_timestamp(self.timestamp),
232
- "status": self.status,
233
- } # type: Dict[str, Any]
234
- if self.errors:
235
- rv["errors"] = self.errors
236
- if self.did is not None:
237
- rv["did"] = self.did
238
- if self.duration is not None:
239
- rv["duration"] = self.duration
240
-
241
- attrs = {}
242
- if self.release is not None:
243
- attrs["release"] = self.release
244
- if self.environment is not None:
245
- attrs["environment"] = self.environment
246
- if self.ip_address is not None:
247
- attrs["ip_address"] = self.ip_address
248
- if self.user_agent is not None:
249
- attrs["user_agent"] = self.user_agent
250
- if attrs:
251
- rv["attrs"] = attrs
252
- return rv
275
+ self.__shutdown_requested.set()