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.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +81 -19
- sentry_sdk/_types.py +311 -11
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +35 -28
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +20 -11
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +71 -60
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +126 -125
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {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
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
import sentry_sdk
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
13
12
|
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
31
|
-
should_track =
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
148
|
+
|
|
149
|
+
class SessionFlusher:
|
|
64
150
|
def __init__(
|
|
65
151
|
self,
|
|
66
|
-
|
|
67
|
-
flush_interval=
|
|
152
|
+
capture_func, # type: Callable[[Envelope], None]
|
|
153
|
+
flush_interval=60, # type: int
|
|
68
154
|
):
|
|
69
155
|
# type: (...) -> None
|
|
70
|
-
self.
|
|
156
|
+
self.capture_func = capture_func
|
|
71
157
|
self.flush_interval = flush_interval
|
|
72
|
-
self.
|
|
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.
|
|
164
|
+
self.__shutdown_requested = Event()
|
|
77
165
|
|
|
78
166
|
def flush(self):
|
|
79
167
|
# type: (...) -> None
|
|
80
|
-
|
|
81
|
-
self.
|
|
82
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
107
|
-
self,
|
|
230
|
+
def add_aggregate_session(
|
|
231
|
+
self,
|
|
232
|
+
session, # type: Session
|
|
108
233
|
):
|
|
109
234
|
# type: (...) -> None
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
#
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
def __init__(
|
|
262
|
+
def add_session(
|
|
124
263
|
self,
|
|
125
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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()
|