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.
- sentry_sdk/__init__.py +48 -30
- sentry_sdk/_compat.py +74 -61
- 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 +289 -0
- sentry_sdk/_types.py +338 -0
- 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 +496 -80
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +1023 -103
- sentry_sdk/consts.py +1438 -66
- 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 +15 -14
- sentry_sdk/envelope.py +369 -0
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +611 -280
- sentry_sdk/integrations/__init__.py +276 -49
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +180 -44
- sentry_sdk/integrations/aiohttp.py +291 -42
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +9 -8
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +341 -0
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +17 -10
- sentry_sdk/integrations/aws_lambda.py +377 -62
- sentry_sdk/integrations/beam.py +176 -0
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +221 -0
- 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 +134 -0
- 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 +48 -14
- sentry_sdk/integrations/django/__init__.py +584 -191
- sentry_sdk/integrations/django/asgi.py +245 -0
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +187 -0
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +79 -5
- sentry_sdk/integrations/django/transactions.py +49 -22
- sentry_sdk/integrations/django/views.py +96 -0
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +50 -13
- sentry_sdk/integrations/executing.py +67 -0
- sentry_sdk/integrations/falcon.py +272 -0
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +142 -88
- sentry_sdk/integrations/gcp.py +239 -0
- sentry_sdk/integrations/gnu_backtrace.py +99 -0
- 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 +307 -96
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +14 -31
- 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 +141 -0
- 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 +112 -68
- 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 +95 -37
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +294 -123
- sentry_sdk/integrations/serverless.py +48 -19
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/__init__.py +4 -0
- sentry_sdk/integrations/spark/spark_driver.py +316 -0
- sentry_sdk/integrations/spark/spark_worker.py +116 -0
- sentry_sdk/integrations/sqlalchemy.py +142 -0
- 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 +235 -29
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +158 -28
- sentry_sdk/integrations/tornado.py +84 -52
- sentry_sdk/integrations/trytond.py +50 -0
- 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 +201 -119
- 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/py.typed +0 -0
- sentry_sdk/scope.py +1713 -85
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +405 -0
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +275 -0
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1486 -0
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +806 -134
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1625 -465
- sentry_sdk/worker.py +54 -25
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.7.5.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/integrations/celery.py +0 -119
- sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
- sentry_sdk-0.7.5.dist-info/METADATA +0 -36
- sentry_sdk-0.7.5.dist-info/RECORD +0 -39
- {sentry_sdk-0.7.5.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
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import warnings
|
|
3
|
+
from threading import Thread, Lock, Event
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
|
|
6
|
+
import sentry_sdk
|
|
7
|
+
from sentry_sdk.envelope import Envelope
|
|
8
|
+
from sentry_sdk.session import Session
|
|
9
|
+
from sentry_sdk.utils import format_timestamp
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from typing import Any
|
|
15
|
+
from typing import Callable
|
|
16
|
+
from typing import Dict
|
|
17
|
+
from typing import Generator
|
|
18
|
+
from typing import List
|
|
19
|
+
from typing import Optional
|
|
20
|
+
from typing import Union
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_auto_session_tracking_enabled(hub=None):
|
|
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
|
+
|
|
35
|
+
if hub is None:
|
|
36
|
+
hub = sentry_sdk.Hub.current
|
|
37
|
+
|
|
38
|
+
should_track = hub.scope._force_auto_session_tracking
|
|
39
|
+
|
|
40
|
+
if should_track is None:
|
|
41
|
+
client_options = hub.client.options if hub.client else {}
|
|
42
|
+
should_track = client_options.get("auto_session_tracking", False)
|
|
43
|
+
|
|
44
|
+
return should_track
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@contextmanager
|
|
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
|
+
|
|
60
|
+
if hub is None:
|
|
61
|
+
hub = sentry_sdk.Hub.current
|
|
62
|
+
with warnings.catch_warnings():
|
|
63
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
64
|
+
should_track = is_auto_session_tracking_enabled(hub)
|
|
65
|
+
if should_track:
|
|
66
|
+
hub.start_session(session_mode=session_mode)
|
|
67
|
+
try:
|
|
68
|
+
yield
|
|
69
|
+
finally:
|
|
70
|
+
if should_track:
|
|
71
|
+
hub.end_session()
|
|
72
|
+
|
|
73
|
+
|
|
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()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
TERMINAL_SESSION_STATES = ("exited", "abnormal", "crashed")
|
|
141
|
+
MAX_ENVELOPE_ITEMS = 100
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def make_aggregate_envelope(aggregate_states, attrs):
|
|
145
|
+
# type: (Any, Any) -> Any
|
|
146
|
+
return {"attrs": dict(attrs), "aggregates": list(aggregate_states.values())}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class SessionFlusher:
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
capture_func, # type: Callable[[Envelope], None]
|
|
153
|
+
flush_interval=60, # type: int
|
|
154
|
+
):
|
|
155
|
+
# type: (...) -> None
|
|
156
|
+
self.capture_func = capture_func
|
|
157
|
+
self.flush_interval = flush_interval
|
|
158
|
+
self.pending_sessions = [] # type: List[Any]
|
|
159
|
+
self.pending_aggregates = {} # type: Dict[Any, Any]
|
|
160
|
+
self._thread = None # type: Optional[Thread]
|
|
161
|
+
self._thread_lock = Lock()
|
|
162
|
+
self._aggregate_lock = Lock()
|
|
163
|
+
self._thread_for_pid = None # type: Optional[int]
|
|
164
|
+
self.__shutdown_requested = Event()
|
|
165
|
+
|
|
166
|
+
def flush(self):
|
|
167
|
+
# type: (...) -> None
|
|
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)
|
|
192
|
+
|
|
193
|
+
def _ensure_running(self):
|
|
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
|
+
"""
|
|
202
|
+
if self._thread_for_pid == os.getpid() and self._thread is not None:
|
|
203
|
+
return None
|
|
204
|
+
with self._thread_lock:
|
|
205
|
+
if self._thread_for_pid == os.getpid() and self._thread is not None:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def _thread():
|
|
209
|
+
# type: (...) -> None
|
|
210
|
+
running = True
|
|
211
|
+
while running:
|
|
212
|
+
running = not self.__shutdown_requested.wait(self.flush_interval)
|
|
213
|
+
self.flush()
|
|
214
|
+
|
|
215
|
+
thread = Thread(target=_thread)
|
|
216
|
+
thread.daemon = True
|
|
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
|
+
|
|
225
|
+
self._thread = thread
|
|
226
|
+
self._thread_for_pid = os.getpid()
|
|
227
|
+
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def add_aggregate_session(
|
|
231
|
+
self,
|
|
232
|
+
session, # type: Session
|
|
233
|
+
):
|
|
234
|
+
# type: (...) -> None
|
|
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.
|
|
240
|
+
|
|
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, {})
|
|
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
|
|
261
|
+
|
|
262
|
+
def add_session(
|
|
263
|
+
self,
|
|
264
|
+
session, # type: Session
|
|
265
|
+
):
|
|
266
|
+
# type: (...) -> None
|
|
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):
|
|
274
|
+
# type: (...) -> None
|
|
275
|
+
self.__shutdown_requested.set()
|
sentry_sdk/spotlight.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import urllib.parse
|
|
5
|
+
import urllib.request
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib3
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from itertools import chain, product
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Any
|
|
16
|
+
from typing import Callable
|
|
17
|
+
from typing import Dict
|
|
18
|
+
from typing import Optional
|
|
19
|
+
from typing import Self
|
|
20
|
+
|
|
21
|
+
from sentry_sdk.utils import (
|
|
22
|
+
logger as sentry_logger,
|
|
23
|
+
env_to_bool,
|
|
24
|
+
capture_internal_exceptions,
|
|
25
|
+
)
|
|
26
|
+
from sentry_sdk.envelope import Envelope
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("spotlight")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
DEFAULT_SPOTLIGHT_URL = "http://localhost:8969/stream"
|
|
33
|
+
DJANGO_SPOTLIGHT_MIDDLEWARE_PATH = "sentry_sdk.spotlight.SpotlightMiddleware"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SpotlightClient:
|
|
37
|
+
def __init__(self, url):
|
|
38
|
+
# type: (str) -> None
|
|
39
|
+
self.url = url
|
|
40
|
+
self.http = urllib3.PoolManager()
|
|
41
|
+
self.fails = 0
|
|
42
|
+
|
|
43
|
+
def capture_envelope(self, envelope):
|
|
44
|
+
# type: (Envelope) -> None
|
|
45
|
+
body = io.BytesIO()
|
|
46
|
+
envelope.serialize_into(body)
|
|
47
|
+
try:
|
|
48
|
+
req = self.http.request(
|
|
49
|
+
url=self.url,
|
|
50
|
+
body=body.getvalue(),
|
|
51
|
+
method="POST",
|
|
52
|
+
headers={
|
|
53
|
+
"Content-Type": "application/x-sentry-envelope",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
req.close()
|
|
57
|
+
self.fails = 0
|
|
58
|
+
except Exception as e:
|
|
59
|
+
if self.fails < 2:
|
|
60
|
+
sentry_logger.warning(str(e))
|
|
61
|
+
self.fails += 1
|
|
62
|
+
elif self.fails == 2:
|
|
63
|
+
self.fails += 1
|
|
64
|
+
sentry_logger.warning(
|
|
65
|
+
"Looks like Spotlight is not running, will keep trying to send events but will not log errors."
|
|
66
|
+
)
|
|
67
|
+
# omitting self.fails += 1 in the `else:` case intentionally
|
|
68
|
+
# to avoid overflowing the variable if Spotlight never becomes reachable
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
73
|
+
from django.http import HttpResponseServerError, HttpResponse, HttpRequest
|
|
74
|
+
from django.conf import settings
|
|
75
|
+
|
|
76
|
+
SPOTLIGHT_JS_ENTRY_PATH = "/assets/main.js"
|
|
77
|
+
SPOTLIGHT_JS_SNIPPET_PATTERN = (
|
|
78
|
+
"<script>window.__spotlight = {{ initOptions: {{ sidecarUrl: '{spotlight_url}', fullPage: false }} }};</script>\n"
|
|
79
|
+
'<script type="module" crossorigin src="{spotlight_js_url}"></script>\n'
|
|
80
|
+
)
|
|
81
|
+
SPOTLIGHT_ERROR_PAGE_SNIPPET = (
|
|
82
|
+
'<html><base href="{spotlight_url}">\n'
|
|
83
|
+
'<script>window.__spotlight = {{ initOptions: {{ fullPage: true, startFrom: "/errors/{event_id}" }}}};</script>\n'
|
|
84
|
+
)
|
|
85
|
+
CHARSET_PREFIX = "charset="
|
|
86
|
+
BODY_TAG_NAME = "body"
|
|
87
|
+
BODY_CLOSE_TAG_POSSIBILITIES = tuple(
|
|
88
|
+
"</{}>".format("".join(chars))
|
|
89
|
+
for chars in product(*zip(BODY_TAG_NAME.upper(), BODY_TAG_NAME.lower()))
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
class SpotlightMiddleware(MiddlewareMixin): # type: ignore[misc]
|
|
93
|
+
_spotlight_script = None # type: Optional[str]
|
|
94
|
+
_spotlight_url = None # type: Optional[str]
|
|
95
|
+
|
|
96
|
+
def __init__(self, get_response):
|
|
97
|
+
# type: (Self, Callable[..., HttpResponse]) -> None
|
|
98
|
+
super().__init__(get_response)
|
|
99
|
+
|
|
100
|
+
import sentry_sdk.api
|
|
101
|
+
|
|
102
|
+
self.sentry_sdk = sentry_sdk.api
|
|
103
|
+
|
|
104
|
+
spotlight_client = self.sentry_sdk.get_client().spotlight
|
|
105
|
+
if spotlight_client is None:
|
|
106
|
+
sentry_logger.warning(
|
|
107
|
+
"Cannot find Spotlight client from SpotlightMiddleware, disabling the middleware."
|
|
108
|
+
)
|
|
109
|
+
return None
|
|
110
|
+
# Spotlight URL has a trailing `/stream` part at the end so split it off
|
|
111
|
+
self._spotlight_url = urllib.parse.urljoin(spotlight_client.url, "../")
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def spotlight_script(self):
|
|
115
|
+
# type: (Self) -> Optional[str]
|
|
116
|
+
if self._spotlight_url is not None and self._spotlight_script is None:
|
|
117
|
+
try:
|
|
118
|
+
spotlight_js_url = urllib.parse.urljoin(
|
|
119
|
+
self._spotlight_url, SPOTLIGHT_JS_ENTRY_PATH
|
|
120
|
+
)
|
|
121
|
+
req = urllib.request.Request(
|
|
122
|
+
spotlight_js_url,
|
|
123
|
+
method="HEAD",
|
|
124
|
+
)
|
|
125
|
+
urllib.request.urlopen(req)
|
|
126
|
+
self._spotlight_script = SPOTLIGHT_JS_SNIPPET_PATTERN.format(
|
|
127
|
+
spotlight_url=self._spotlight_url,
|
|
128
|
+
spotlight_js_url=spotlight_js_url,
|
|
129
|
+
)
|
|
130
|
+
except urllib.error.URLError as err:
|
|
131
|
+
sentry_logger.debug(
|
|
132
|
+
"Cannot get Spotlight JS to inject at %s. SpotlightMiddleware will not be very useful.",
|
|
133
|
+
spotlight_js_url,
|
|
134
|
+
exc_info=err,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return self._spotlight_script
|
|
138
|
+
|
|
139
|
+
def process_response(self, _request, response):
|
|
140
|
+
# type: (Self, HttpRequest, HttpResponse) -> Optional[HttpResponse]
|
|
141
|
+
content_type_header = tuple(
|
|
142
|
+
p.strip()
|
|
143
|
+
for p in response.headers.get("Content-Type", "").lower().split(";")
|
|
144
|
+
)
|
|
145
|
+
content_type = content_type_header[0]
|
|
146
|
+
if len(content_type_header) > 1 and content_type_header[1].startswith(
|
|
147
|
+
CHARSET_PREFIX
|
|
148
|
+
):
|
|
149
|
+
encoding = content_type_header[1][len(CHARSET_PREFIX) :]
|
|
150
|
+
else:
|
|
151
|
+
encoding = "utf-8"
|
|
152
|
+
|
|
153
|
+
if (
|
|
154
|
+
self.spotlight_script is not None
|
|
155
|
+
and not response.streaming
|
|
156
|
+
and content_type == "text/html"
|
|
157
|
+
):
|
|
158
|
+
content_length = len(response.content)
|
|
159
|
+
injection = self.spotlight_script.encode(encoding)
|
|
160
|
+
injection_site = next(
|
|
161
|
+
(
|
|
162
|
+
idx
|
|
163
|
+
for idx in (
|
|
164
|
+
response.content.rfind(body_variant.encode(encoding))
|
|
165
|
+
for body_variant in BODY_CLOSE_TAG_POSSIBILITIES
|
|
166
|
+
)
|
|
167
|
+
if idx > -1
|
|
168
|
+
),
|
|
169
|
+
content_length,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# This approach works even when we don't have a `</body>` tag
|
|
173
|
+
response.content = (
|
|
174
|
+
response.content[:injection_site]
|
|
175
|
+
+ injection
|
|
176
|
+
+ response.content[injection_site:]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if response.has_header("Content-Length"):
|
|
180
|
+
response.headers["Content-Length"] = content_length + len(injection)
|
|
181
|
+
|
|
182
|
+
return response
|
|
183
|
+
|
|
184
|
+
def process_exception(self, _request, exception):
|
|
185
|
+
# type: (Self, HttpRequest, Exception) -> Optional[HttpResponseServerError]
|
|
186
|
+
if not settings.DEBUG or not self._spotlight_url:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
spotlight = (
|
|
191
|
+
urllib.request.urlopen(self._spotlight_url).read().decode("utf-8")
|
|
192
|
+
)
|
|
193
|
+
except urllib.error.URLError:
|
|
194
|
+
return None
|
|
195
|
+
else:
|
|
196
|
+
event_id = self.sentry_sdk.capture_exception(exception)
|
|
197
|
+
return HttpResponseServerError(
|
|
198
|
+
spotlight.replace(
|
|
199
|
+
"<html>",
|
|
200
|
+
SPOTLIGHT_ERROR_PAGE_SNIPPET.format(
|
|
201
|
+
spotlight_url=self._spotlight_url, event_id=event_id
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
except ImportError:
|
|
207
|
+
settings = None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def setup_spotlight(options):
|
|
211
|
+
# type: (Dict[str, Any]) -> Optional[SpotlightClient]
|
|
212
|
+
_handler = logging.StreamHandler(sys.stderr)
|
|
213
|
+
_handler.setFormatter(logging.Formatter(" [spotlight] %(levelname)s: %(message)s"))
|
|
214
|
+
logger.addHandler(_handler)
|
|
215
|
+
logger.setLevel(logging.INFO)
|
|
216
|
+
|
|
217
|
+
url = options.get("spotlight")
|
|
218
|
+
|
|
219
|
+
if url is True:
|
|
220
|
+
url = DEFAULT_SPOTLIGHT_URL
|
|
221
|
+
|
|
222
|
+
if not isinstance(url, str):
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
with capture_internal_exceptions():
|
|
226
|
+
if (
|
|
227
|
+
settings is not None
|
|
228
|
+
and settings.DEBUG
|
|
229
|
+
and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_ON_ERROR", "1"))
|
|
230
|
+
and env_to_bool(os.environ.get("SENTRY_SPOTLIGHT_MIDDLEWARE", "1"))
|
|
231
|
+
):
|
|
232
|
+
middleware = settings.MIDDLEWARE
|
|
233
|
+
if DJANGO_SPOTLIGHT_MIDDLEWARE_PATH not in middleware:
|
|
234
|
+
settings.MIDDLEWARE = type(middleware)(
|
|
235
|
+
chain(middleware, (DJANGO_SPOTLIGHT_MIDDLEWARE_PATH,))
|
|
236
|
+
)
|
|
237
|
+
logger.info("Enabled Spotlight integration for Django")
|
|
238
|
+
|
|
239
|
+
client = SpotlightClient(url)
|
|
240
|
+
logger.info("Enabled Spotlight using sidecar at %s", url)
|
|
241
|
+
|
|
242
|
+
return client
|