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/client.py
CHANGED
|
@@ -1,88 +1,291 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import uuid
|
|
3
3
|
import random
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from itertools import islice
|
|
6
4
|
import socket
|
|
7
|
-
|
|
8
|
-
from
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from typing import TYPE_CHECKING, List, Dict, cast, overload
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
import sentry_sdk
|
|
12
|
+
from sentry_sdk._compat import PY37, check_uwsgi_thread_support
|
|
13
|
+
from sentry_sdk._metrics_batcher import MetricsBatcher
|
|
9
14
|
from sentry_sdk.utils import (
|
|
15
|
+
AnnotatedValue,
|
|
16
|
+
ContextVar,
|
|
10
17
|
capture_internal_exceptions,
|
|
11
18
|
current_stacktrace,
|
|
12
|
-
|
|
19
|
+
env_to_bool,
|
|
13
20
|
format_timestamp,
|
|
21
|
+
get_sdk_name,
|
|
14
22
|
get_type_name,
|
|
23
|
+
get_default_release,
|
|
15
24
|
handle_in_app,
|
|
25
|
+
is_gevent,
|
|
16
26
|
logger,
|
|
27
|
+
get_before_send_log,
|
|
28
|
+
get_before_send_metric,
|
|
29
|
+
has_logs_enabled,
|
|
30
|
+
has_metrics_enabled,
|
|
17
31
|
)
|
|
18
32
|
from sentry_sdk.serializer import serialize
|
|
19
|
-
from sentry_sdk.
|
|
20
|
-
from sentry_sdk.
|
|
21
|
-
from sentry_sdk.
|
|
22
|
-
|
|
33
|
+
from sentry_sdk.tracing import trace
|
|
34
|
+
from sentry_sdk.transport import BaseHttpTransport, make_transport
|
|
35
|
+
from sentry_sdk.consts import (
|
|
36
|
+
SPANDATA,
|
|
37
|
+
DEFAULT_MAX_VALUE_LENGTH,
|
|
38
|
+
DEFAULT_OPTIONS,
|
|
39
|
+
INSTRUMENTER,
|
|
40
|
+
VERSION,
|
|
41
|
+
ClientConstructor,
|
|
42
|
+
)
|
|
43
|
+
from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations
|
|
44
|
+
from sentry_sdk.integrations.dedupe import DedupeIntegration
|
|
23
45
|
from sentry_sdk.sessions import SessionFlusher
|
|
24
|
-
from sentry_sdk.envelope import Envelope
|
|
25
|
-
|
|
26
|
-
from sentry_sdk.
|
|
46
|
+
from sentry_sdk.envelope import Envelope
|
|
47
|
+
from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler
|
|
48
|
+
from sentry_sdk.profiler.transaction_profiler import (
|
|
49
|
+
has_profiling_enabled,
|
|
50
|
+
Profile,
|
|
51
|
+
setup_profiler,
|
|
52
|
+
)
|
|
53
|
+
from sentry_sdk.scrubber import EventScrubber
|
|
54
|
+
from sentry_sdk.monitor import Monitor
|
|
27
55
|
|
|
28
|
-
if
|
|
56
|
+
if TYPE_CHECKING:
|
|
29
57
|
from typing import Any
|
|
30
58
|
from typing import Callable
|
|
31
|
-
from typing import Dict
|
|
32
|
-
from typing import List
|
|
33
59
|
from typing import Optional
|
|
60
|
+
from typing import Sequence
|
|
61
|
+
from typing import Type
|
|
62
|
+
from typing import Union
|
|
63
|
+
from typing import TypeVar
|
|
34
64
|
|
|
65
|
+
from sentry_sdk._types import Event, Hint, SDKInfo, Log, Metric, EventDataCategory
|
|
66
|
+
from sentry_sdk.integrations import Integration
|
|
35
67
|
from sentry_sdk.scope import Scope
|
|
36
|
-
from sentry_sdk.
|
|
37
|
-
from sentry_sdk.
|
|
68
|
+
from sentry_sdk.session import Session
|
|
69
|
+
from sentry_sdk.spotlight import SpotlightClient
|
|
70
|
+
from sentry_sdk.transport import Transport
|
|
71
|
+
from sentry_sdk._log_batcher import LogBatcher
|
|
72
|
+
from sentry_sdk._metrics_batcher import MetricsBatcher
|
|
38
73
|
|
|
74
|
+
I = TypeVar("I", bound=Integration) # noqa: E741
|
|
39
75
|
|
|
40
76
|
_client_init_debug = ContextVar("client_init_debug")
|
|
41
77
|
|
|
42
78
|
|
|
79
|
+
SDK_INFO = {
|
|
80
|
+
"name": "sentry.python", # SDK name will be overridden after integrations have been loaded with sentry_sdk.integrations.setup_integrations()
|
|
81
|
+
"version": VERSION,
|
|
82
|
+
"packages": [{"name": "pypi:sentry-sdk", "version": VERSION}],
|
|
83
|
+
} # type: SDKInfo
|
|
84
|
+
|
|
85
|
+
|
|
43
86
|
def _get_options(*args, **kwargs):
|
|
44
87
|
# type: (*Optional[str], **Any) -> Dict[str, Any]
|
|
45
|
-
if args and (isinstance(args[0], (
|
|
88
|
+
if args and (isinstance(args[0], (bytes, str)) or args[0] is None):
|
|
46
89
|
dsn = args[0] # type: Optional[str]
|
|
47
90
|
args = args[1:]
|
|
48
91
|
else:
|
|
49
92
|
dsn = None
|
|
50
93
|
|
|
94
|
+
if len(args) > 1:
|
|
95
|
+
raise TypeError("Only single positional argument is expected")
|
|
96
|
+
|
|
51
97
|
rv = dict(DEFAULT_OPTIONS)
|
|
52
98
|
options = dict(*args, **kwargs)
|
|
53
99
|
if dsn is not None and options.get("dsn") is None:
|
|
54
100
|
options["dsn"] = dsn
|
|
55
101
|
|
|
56
|
-
for key, value in
|
|
102
|
+
for key, value in options.items():
|
|
57
103
|
if key not in rv:
|
|
58
104
|
raise TypeError("Unknown option %r" % (key,))
|
|
105
|
+
|
|
59
106
|
rv[key] = value
|
|
60
107
|
|
|
61
108
|
if rv["dsn"] is None:
|
|
62
109
|
rv["dsn"] = os.environ.get("SENTRY_DSN")
|
|
63
110
|
|
|
64
111
|
if rv["release"] is None:
|
|
65
|
-
rv["release"] =
|
|
112
|
+
rv["release"] = get_default_release()
|
|
66
113
|
|
|
67
114
|
if rv["environment"] is None:
|
|
68
|
-
rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT")
|
|
115
|
+
rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production"
|
|
116
|
+
|
|
117
|
+
if rv["debug"] is None:
|
|
118
|
+
rv["debug"] = env_to_bool(os.environ.get("SENTRY_DEBUG"), strict=True) or False
|
|
69
119
|
|
|
70
120
|
if rv["server_name"] is None and hasattr(socket, "gethostname"):
|
|
71
121
|
rv["server_name"] = socket.gethostname()
|
|
72
122
|
|
|
123
|
+
if rv["instrumenter"] is None:
|
|
124
|
+
rv["instrumenter"] = INSTRUMENTER.SENTRY
|
|
125
|
+
|
|
126
|
+
if rv["project_root"] is None:
|
|
127
|
+
try:
|
|
128
|
+
project_root = os.getcwd()
|
|
129
|
+
except Exception:
|
|
130
|
+
project_root = None
|
|
131
|
+
|
|
132
|
+
rv["project_root"] = project_root
|
|
133
|
+
|
|
134
|
+
if rv["enable_tracing"] is True and rv["traces_sample_rate"] is None:
|
|
135
|
+
rv["traces_sample_rate"] = 1.0
|
|
136
|
+
|
|
137
|
+
if rv["event_scrubber"] is None:
|
|
138
|
+
rv["event_scrubber"] = EventScrubber(
|
|
139
|
+
send_default_pii=(
|
|
140
|
+
False if rv["send_default_pii"] is None else rv["send_default_pii"]
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if rv["socket_options"] and not isinstance(rv["socket_options"], list):
|
|
145
|
+
logger.warning(
|
|
146
|
+
"Ignoring socket_options because of unexpected format. See urllib3.HTTPConnection.socket_options for the expected format."
|
|
147
|
+
)
|
|
148
|
+
rv["socket_options"] = None
|
|
149
|
+
|
|
150
|
+
if rv["keep_alive"] is None:
|
|
151
|
+
rv["keep_alive"] = (
|
|
152
|
+
env_to_bool(os.environ.get("SENTRY_KEEP_ALIVE"), strict=True) or False
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if rv["enable_tracing"] is not None:
|
|
156
|
+
warnings.warn(
|
|
157
|
+
"The `enable_tracing` parameter is deprecated. Please use `traces_sample_rate` instead.",
|
|
158
|
+
DeprecationWarning,
|
|
159
|
+
stacklevel=2,
|
|
160
|
+
)
|
|
161
|
+
|
|
73
162
|
return rv
|
|
74
163
|
|
|
75
164
|
|
|
76
|
-
|
|
77
|
-
|
|
165
|
+
try:
|
|
166
|
+
# Python 3.6+
|
|
167
|
+
module_not_found_error = ModuleNotFoundError
|
|
168
|
+
except Exception:
|
|
169
|
+
# Older Python versions
|
|
170
|
+
module_not_found_error = ImportError # type: ignore
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class BaseClient:
|
|
174
|
+
"""
|
|
175
|
+
.. versionadded:: 2.0.0
|
|
176
|
+
|
|
177
|
+
The basic definition of a client that is used for sending data to Sentry.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
spotlight = None # type: Optional[SpotlightClient]
|
|
181
|
+
|
|
182
|
+
def __init__(self, options=None):
|
|
183
|
+
# type: (Optional[Dict[str, Any]]) -> None
|
|
184
|
+
self.options = options if options is not None else DEFAULT_OPTIONS # type: Dict[str, Any]
|
|
185
|
+
|
|
186
|
+
self.transport = None # type: Optional[Transport]
|
|
187
|
+
self.monitor = None # type: Optional[Monitor]
|
|
188
|
+
self.log_batcher = None # type: Optional[LogBatcher]
|
|
189
|
+
self.metrics_batcher = None # type: Optional[MetricsBatcher]
|
|
190
|
+
|
|
191
|
+
def __getstate__(self, *args, **kwargs):
|
|
192
|
+
# type: (*Any, **Any) -> Any
|
|
193
|
+
return {"options": {}}
|
|
194
|
+
|
|
195
|
+
def __setstate__(self, *args, **kwargs):
|
|
196
|
+
# type: (*Any, **Any) -> None
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def dsn(self):
|
|
201
|
+
# type: () -> Optional[str]
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
def should_send_default_pii(self):
|
|
205
|
+
# type: () -> bool
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
def is_active(self):
|
|
209
|
+
# type: () -> bool
|
|
210
|
+
"""
|
|
211
|
+
.. versionadded:: 2.0.0
|
|
212
|
+
|
|
213
|
+
Returns whether the client is active (able to send data to Sentry)
|
|
214
|
+
"""
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
def capture_event(self, *args, **kwargs):
|
|
218
|
+
# type: (*Any, **Any) -> Optional[str]
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def _capture_log(self, log):
|
|
222
|
+
# type: (Log) -> None
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
def _capture_metric(self, metric):
|
|
226
|
+
# type: (Metric) -> None
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
def capture_session(self, *args, **kwargs):
|
|
230
|
+
# type: (*Any, **Any) -> None
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
if TYPE_CHECKING:
|
|
234
|
+
|
|
235
|
+
@overload
|
|
236
|
+
def get_integration(self, name_or_class):
|
|
237
|
+
# type: (str) -> Optional[Integration]
|
|
238
|
+
...
|
|
239
|
+
|
|
240
|
+
@overload
|
|
241
|
+
def get_integration(self, name_or_class):
|
|
242
|
+
# type: (type[I]) -> Optional[I]
|
|
243
|
+
...
|
|
244
|
+
|
|
245
|
+
def get_integration(self, name_or_class):
|
|
246
|
+
# type: (Union[str, type[Integration]]) -> Optional[Integration]
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
def close(self, *args, **kwargs):
|
|
250
|
+
# type: (*Any, **Any) -> None
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
def flush(self, *args, **kwargs):
|
|
254
|
+
# type: (*Any, **Any) -> None
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def __enter__(self):
|
|
258
|
+
# type: () -> BaseClient
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
def __exit__(self, exc_type, exc_value, tb):
|
|
262
|
+
# type: (Any, Any, Any) -> None
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class NonRecordingClient(BaseClient):
|
|
267
|
+
"""
|
|
268
|
+
.. versionadded:: 2.0.0
|
|
269
|
+
|
|
270
|
+
A client that does not send any events to Sentry. This is used as a fallback when the Sentry SDK is not yet initialized.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class _Client(BaseClient):
|
|
277
|
+
"""
|
|
278
|
+
The client is internally responsible for capturing the events and
|
|
78
279
|
forwarding them to sentry through the configured transport. It takes
|
|
79
280
|
the client options as keyword arguments and optionally the DSN as first
|
|
80
281
|
argument.
|
|
282
|
+
|
|
283
|
+
Alias of :py:class:`sentry_sdk.Client`. (Was created for better intelisense support)
|
|
81
284
|
"""
|
|
82
285
|
|
|
83
286
|
def __init__(self, *args, **kwargs):
|
|
84
287
|
# type: (*Any, **Any) -> None
|
|
85
|
-
self.options
|
|
288
|
+
super(_Client, self).__init__(options=get_options(*args, **kwargs))
|
|
86
289
|
self._init_impl()
|
|
87
290
|
|
|
88
291
|
def __getstate__(self):
|
|
@@ -94,47 +297,213 @@ class _Client(object):
|
|
|
94
297
|
self.options = state["options"]
|
|
95
298
|
self._init_impl()
|
|
96
299
|
|
|
300
|
+
def _setup_instrumentation(self, functions_to_trace):
|
|
301
|
+
# type: (Sequence[Dict[str, str]]) -> None
|
|
302
|
+
"""
|
|
303
|
+
Instruments the functions given in the list `functions_to_trace` with the `@sentry_sdk.tracing.trace` decorator.
|
|
304
|
+
"""
|
|
305
|
+
for function in functions_to_trace:
|
|
306
|
+
class_name = None
|
|
307
|
+
function_qualname = function["qualified_name"]
|
|
308
|
+
module_name, function_name = function_qualname.rsplit(".", 1)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
# Try to import module and function
|
|
312
|
+
# ex: "mymodule.submodule.funcname"
|
|
313
|
+
|
|
314
|
+
module_obj = import_module(module_name)
|
|
315
|
+
function_obj = getattr(module_obj, function_name)
|
|
316
|
+
setattr(module_obj, function_name, trace(function_obj))
|
|
317
|
+
logger.debug("Enabled tracing for %s", function_qualname)
|
|
318
|
+
except module_not_found_error:
|
|
319
|
+
try:
|
|
320
|
+
# Try to import a class
|
|
321
|
+
# ex: "mymodule.submodule.MyClassName.member_function"
|
|
322
|
+
|
|
323
|
+
module_name, class_name = module_name.rsplit(".", 1)
|
|
324
|
+
module_obj = import_module(module_name)
|
|
325
|
+
class_obj = getattr(module_obj, class_name)
|
|
326
|
+
function_obj = getattr(class_obj, function_name)
|
|
327
|
+
function_type = type(class_obj.__dict__[function_name])
|
|
328
|
+
traced_function = trace(function_obj)
|
|
329
|
+
|
|
330
|
+
if function_type in (staticmethod, classmethod):
|
|
331
|
+
traced_function = staticmethod(traced_function)
|
|
332
|
+
|
|
333
|
+
setattr(class_obj, function_name, traced_function)
|
|
334
|
+
setattr(module_obj, class_name, class_obj)
|
|
335
|
+
logger.debug("Enabled tracing for %s", function_qualname)
|
|
336
|
+
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.warning(
|
|
339
|
+
"Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
|
|
340
|
+
function_qualname,
|
|
341
|
+
e,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
except Exception as e:
|
|
345
|
+
logger.warning(
|
|
346
|
+
"Can not enable tracing for '%s'. (%s) Please check your `functions_to_trace` parameter.",
|
|
347
|
+
function_qualname,
|
|
348
|
+
e,
|
|
349
|
+
)
|
|
350
|
+
|
|
97
351
|
def _init_impl(self):
|
|
98
352
|
# type: () -> None
|
|
99
353
|
old_debug = _client_init_debug.get(False)
|
|
100
354
|
|
|
101
|
-
def
|
|
102
|
-
# type: (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
transport.
|
|
355
|
+
def _capture_envelope(envelope):
|
|
356
|
+
# type: (Envelope) -> None
|
|
357
|
+
if self.transport is not None:
|
|
358
|
+
self.transport.capture_envelope(envelope)
|
|
359
|
+
|
|
360
|
+
def _record_lost_event(
|
|
361
|
+
reason, # type: str
|
|
362
|
+
data_category, # type: EventDataCategory
|
|
363
|
+
quantity=1, # type: int
|
|
364
|
+
):
|
|
365
|
+
# type: (...) -> None
|
|
366
|
+
if self.transport is not None:
|
|
367
|
+
self.transport.record_lost_event(
|
|
368
|
+
reason=reason,
|
|
369
|
+
data_category=data_category,
|
|
370
|
+
quantity=quantity,
|
|
371
|
+
)
|
|
114
372
|
|
|
115
373
|
try:
|
|
116
374
|
_client_init_debug.set(self.options["debug"])
|
|
117
375
|
self.transport = make_transport(self.options)
|
|
118
|
-
self.session_flusher = SessionFlusher(flush_func=_send_sessions)
|
|
119
376
|
|
|
120
|
-
|
|
121
|
-
if self.
|
|
377
|
+
self.monitor = None
|
|
378
|
+
if self.transport:
|
|
379
|
+
if self.options["enable_backpressure_handling"]:
|
|
380
|
+
self.monitor = Monitor(self.transport)
|
|
381
|
+
|
|
382
|
+
self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
|
|
383
|
+
|
|
384
|
+
self.log_batcher = None
|
|
385
|
+
|
|
386
|
+
if has_logs_enabled(self.options):
|
|
387
|
+
from sentry_sdk._log_batcher import LogBatcher
|
|
388
|
+
|
|
389
|
+
self.log_batcher = LogBatcher(
|
|
390
|
+
capture_func=_capture_envelope,
|
|
391
|
+
record_lost_func=_record_lost_event,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
self.metrics_batcher = None
|
|
395
|
+
if has_metrics_enabled(self.options):
|
|
396
|
+
self.metrics_batcher = MetricsBatcher(
|
|
397
|
+
capture_func=_capture_envelope,
|
|
398
|
+
record_lost_func=_record_lost_event,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
max_request_body_size = ("always", "never", "small", "medium")
|
|
402
|
+
if self.options["max_request_body_size"] not in max_request_body_size:
|
|
122
403
|
raise ValueError(
|
|
123
|
-
"Invalid value for
|
|
124
|
-
|
|
404
|
+
"Invalid value for max_request_body_size. Must be one of {}".format(
|
|
405
|
+
max_request_body_size
|
|
125
406
|
)
|
|
126
407
|
)
|
|
127
408
|
|
|
409
|
+
if self.options["_experiments"].get("otel_powered_performance", False):
|
|
410
|
+
logger.debug(
|
|
411
|
+
"[OTel] Enabling experimental OTel-powered performance monitoring."
|
|
412
|
+
)
|
|
413
|
+
self.options["instrumenter"] = INSTRUMENTER.OTEL
|
|
414
|
+
if (
|
|
415
|
+
"sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration"
|
|
416
|
+
not in _DEFAULT_INTEGRATIONS
|
|
417
|
+
):
|
|
418
|
+
_DEFAULT_INTEGRATIONS.append(
|
|
419
|
+
"sentry_sdk.integrations.opentelemetry.integration.OpenTelemetryIntegration",
|
|
420
|
+
)
|
|
421
|
+
|
|
128
422
|
self.integrations = setup_integrations(
|
|
129
423
|
self.options["integrations"],
|
|
130
424
|
with_defaults=self.options["default_integrations"],
|
|
131
|
-
with_auto_enabling_integrations=self.options[
|
|
132
|
-
"auto_enabling_integrations"
|
|
133
|
-
|
|
425
|
+
with_auto_enabling_integrations=self.options[
|
|
426
|
+
"auto_enabling_integrations"
|
|
427
|
+
],
|
|
428
|
+
disabled_integrations=self.options["disabled_integrations"],
|
|
429
|
+
options=self.options,
|
|
134
430
|
)
|
|
431
|
+
|
|
432
|
+
spotlight_config = self.options.get("spotlight")
|
|
433
|
+
if spotlight_config is None and "SENTRY_SPOTLIGHT" in os.environ:
|
|
434
|
+
spotlight_env_value = os.environ["SENTRY_SPOTLIGHT"]
|
|
435
|
+
spotlight_config = env_to_bool(spotlight_env_value, strict=True)
|
|
436
|
+
self.options["spotlight"] = (
|
|
437
|
+
spotlight_config
|
|
438
|
+
if spotlight_config is not None
|
|
439
|
+
else spotlight_env_value
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
if self.options.get("spotlight"):
|
|
443
|
+
# This is intentionally here to prevent setting up spotlight
|
|
444
|
+
# stuff we don't need unless spotlight is explicitly enabled
|
|
445
|
+
from sentry_sdk.spotlight import setup_spotlight
|
|
446
|
+
|
|
447
|
+
self.spotlight = setup_spotlight(self.options)
|
|
448
|
+
if not self.options["dsn"]:
|
|
449
|
+
sample_all = lambda *_args, **_kwargs: 1.0
|
|
450
|
+
self.options["send_default_pii"] = True
|
|
451
|
+
self.options["error_sampler"] = sample_all
|
|
452
|
+
self.options["traces_sampler"] = sample_all
|
|
453
|
+
self.options["profiles_sampler"] = sample_all
|
|
454
|
+
|
|
455
|
+
sdk_name = get_sdk_name(list(self.integrations.keys()))
|
|
456
|
+
SDK_INFO["name"] = sdk_name
|
|
457
|
+
logger.debug("Setting SDK name to '%s'", sdk_name)
|
|
458
|
+
|
|
459
|
+
if has_profiling_enabled(self.options):
|
|
460
|
+
try:
|
|
461
|
+
setup_profiler(self.options)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
logger.debug("Can not set up profiler. (%s)", e)
|
|
464
|
+
else:
|
|
465
|
+
try:
|
|
466
|
+
setup_continuous_profiler(
|
|
467
|
+
self.options,
|
|
468
|
+
sdk_info=SDK_INFO,
|
|
469
|
+
capture_func=_capture_envelope,
|
|
470
|
+
)
|
|
471
|
+
except Exception as e:
|
|
472
|
+
logger.debug("Can not set up continuous profiler. (%s)", e)
|
|
473
|
+
|
|
135
474
|
finally:
|
|
136
475
|
_client_init_debug.set(old_debug)
|
|
137
476
|
|
|
477
|
+
self._setup_instrumentation(self.options.get("functions_to_trace", []))
|
|
478
|
+
|
|
479
|
+
if (
|
|
480
|
+
self.monitor
|
|
481
|
+
or self.log_batcher
|
|
482
|
+
or has_profiling_enabled(self.options)
|
|
483
|
+
or isinstance(self.transport, BaseHttpTransport)
|
|
484
|
+
):
|
|
485
|
+
# If we have anything on that could spawn a background thread, we
|
|
486
|
+
# need to check if it's safe to use them.
|
|
487
|
+
check_uwsgi_thread_support()
|
|
488
|
+
|
|
489
|
+
def is_active(self):
|
|
490
|
+
# type: () -> bool
|
|
491
|
+
"""
|
|
492
|
+
.. versionadded:: 2.0.0
|
|
493
|
+
|
|
494
|
+
Returns whether the client is active (able to send data to Sentry)
|
|
495
|
+
"""
|
|
496
|
+
return True
|
|
497
|
+
|
|
498
|
+
def should_send_default_pii(self):
|
|
499
|
+
# type: () -> bool
|
|
500
|
+
"""
|
|
501
|
+
.. versionadded:: 2.0.0
|
|
502
|
+
|
|
503
|
+
Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry.
|
|
504
|
+
"""
|
|
505
|
+
return self.options.get("send_default_pii") or False
|
|
506
|
+
|
|
138
507
|
@property
|
|
139
508
|
def dsn(self):
|
|
140
509
|
# type: () -> Optional[str]
|
|
@@ -144,24 +513,64 @@ class _Client(object):
|
|
|
144
513
|
def _prepare_event(
|
|
145
514
|
self,
|
|
146
515
|
event, # type: Event
|
|
147
|
-
hint, # type:
|
|
516
|
+
hint, # type: Hint
|
|
148
517
|
scope, # type: Optional[Scope]
|
|
149
518
|
):
|
|
150
519
|
# type: (...) -> Optional[Event]
|
|
151
520
|
|
|
521
|
+
previous_total_spans = None # type: Optional[int]
|
|
522
|
+
previous_total_breadcrumbs = None # type: Optional[int]
|
|
523
|
+
|
|
152
524
|
if event.get("timestamp") is None:
|
|
153
|
-
event["timestamp"] = datetime.
|
|
525
|
+
event["timestamp"] = datetime.now(timezone.utc)
|
|
154
526
|
|
|
155
|
-
|
|
527
|
+
is_transaction = event.get("type") == "transaction"
|
|
156
528
|
|
|
157
529
|
if scope is not None:
|
|
158
|
-
|
|
530
|
+
spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
|
|
531
|
+
event_ = scope.apply_to_event(event, hint, self.options)
|
|
532
|
+
|
|
533
|
+
# one of the event/error processors returned None
|
|
159
534
|
if event_ is None:
|
|
535
|
+
if self.transport:
|
|
536
|
+
self.transport.record_lost_event(
|
|
537
|
+
"event_processor",
|
|
538
|
+
data_category=("transaction" if is_transaction else "error"),
|
|
539
|
+
)
|
|
540
|
+
if is_transaction:
|
|
541
|
+
self.transport.record_lost_event(
|
|
542
|
+
"event_processor",
|
|
543
|
+
data_category="span",
|
|
544
|
+
quantity=spans_before + 1, # +1 for the transaction itself
|
|
545
|
+
)
|
|
160
546
|
return None
|
|
547
|
+
|
|
161
548
|
event = event_
|
|
549
|
+
spans_delta = spans_before - len(
|
|
550
|
+
cast(List[Dict[str, object]], event.get("spans", []))
|
|
551
|
+
)
|
|
552
|
+
if is_transaction and spans_delta > 0 and self.transport is not None:
|
|
553
|
+
self.transport.record_lost_event(
|
|
554
|
+
"event_processor", data_category="span", quantity=spans_delta
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
dropped_spans = event.pop("_dropped_spans", 0) + spans_delta # type: int
|
|
558
|
+
if dropped_spans > 0:
|
|
559
|
+
previous_total_spans = spans_before + dropped_spans
|
|
560
|
+
if scope._n_breadcrumbs_truncated > 0:
|
|
561
|
+
breadcrumbs = event.get("breadcrumbs", {})
|
|
562
|
+
values = (
|
|
563
|
+
breadcrumbs.get("values", [])
|
|
564
|
+
if not isinstance(breadcrumbs, AnnotatedValue)
|
|
565
|
+
else []
|
|
566
|
+
)
|
|
567
|
+
previous_total_breadcrumbs = (
|
|
568
|
+
len(values) + scope._n_breadcrumbs_truncated
|
|
569
|
+
)
|
|
162
570
|
|
|
163
571
|
if (
|
|
164
|
-
|
|
572
|
+
not is_transaction
|
|
573
|
+
and self.options["attach_stacktrace"]
|
|
165
574
|
and "exception" not in event
|
|
166
575
|
and "stacktrace" not in event
|
|
167
576
|
and "threads" not in event
|
|
@@ -171,7 +580,12 @@ class _Client(object):
|
|
|
171
580
|
"values": [
|
|
172
581
|
{
|
|
173
582
|
"stacktrace": current_stacktrace(
|
|
174
|
-
self.options
|
|
583
|
+
include_local_variables=self.options.get(
|
|
584
|
+
"include_local_variables", True
|
|
585
|
+
),
|
|
586
|
+
max_value_length=self.options.get(
|
|
587
|
+
"max_value_length", DEFAULT_MAX_VALUE_LENGTH
|
|
588
|
+
),
|
|
175
589
|
),
|
|
176
590
|
"crashed": False,
|
|
177
591
|
"current": True,
|
|
@@ -181,7 +595,7 @@ class _Client(object):
|
|
|
181
595
|
|
|
182
596
|
for key in "release", "environment", "server_name", "dist":
|
|
183
597
|
if event.get(key) is None and self.options[key] is not None:
|
|
184
|
-
event[key] =
|
|
598
|
+
event[key] = str(self.options[key]).strip()
|
|
185
599
|
if event.get("sdk") is None:
|
|
186
600
|
sdk_info = dict(SDK_INFO)
|
|
187
601
|
sdk_info["integrations"] = sorted(self.integrations.keys())
|
|
@@ -191,27 +605,109 @@ class _Client(object):
|
|
|
191
605
|
event["platform"] = "python"
|
|
192
606
|
|
|
193
607
|
event = handle_in_app(
|
|
194
|
-
event,
|
|
608
|
+
event,
|
|
609
|
+
self.options["in_app_exclude"],
|
|
610
|
+
self.options["in_app_include"],
|
|
611
|
+
self.options["project_root"],
|
|
195
612
|
)
|
|
196
613
|
|
|
614
|
+
if event is not None:
|
|
615
|
+
event_scrubber = self.options["event_scrubber"]
|
|
616
|
+
if event_scrubber:
|
|
617
|
+
event_scrubber.scrub_event(event)
|
|
618
|
+
|
|
619
|
+
if scope is not None and scope._gen_ai_original_message_count:
|
|
620
|
+
spans = event.get("spans", []) # type: List[Dict[str, Any]] | AnnotatedValue
|
|
621
|
+
if isinstance(spans, list):
|
|
622
|
+
for span in spans:
|
|
623
|
+
span_id = span.get("span_id", None)
|
|
624
|
+
span_data = span.get("data", {})
|
|
625
|
+
if (
|
|
626
|
+
span_id
|
|
627
|
+
and span_id in scope._gen_ai_original_message_count
|
|
628
|
+
and SPANDATA.GEN_AI_REQUEST_MESSAGES in span_data
|
|
629
|
+
):
|
|
630
|
+
span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES] = AnnotatedValue(
|
|
631
|
+
span_data[SPANDATA.GEN_AI_REQUEST_MESSAGES],
|
|
632
|
+
{"len": scope._gen_ai_original_message_count[span_id]},
|
|
633
|
+
)
|
|
634
|
+
if previous_total_spans is not None:
|
|
635
|
+
event["spans"] = AnnotatedValue(
|
|
636
|
+
event.get("spans", []), {"len": previous_total_spans}
|
|
637
|
+
)
|
|
638
|
+
if previous_total_breadcrumbs is not None:
|
|
639
|
+
event["breadcrumbs"] = AnnotatedValue(
|
|
640
|
+
event.get("breadcrumbs", {"values": []}),
|
|
641
|
+
{"len": previous_total_breadcrumbs},
|
|
642
|
+
)
|
|
643
|
+
|
|
197
644
|
# Postprocess the event here so that annotated types do
|
|
198
645
|
# generally not surface in before_send
|
|
199
646
|
if event is not None:
|
|
200
|
-
event =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"
|
|
647
|
+
event = cast(
|
|
648
|
+
"Event",
|
|
649
|
+
serialize(
|
|
650
|
+
cast("Dict[str, Any]", event),
|
|
651
|
+
max_request_body_size=self.options.get("max_request_body_size"),
|
|
652
|
+
max_value_length=self.options.get("max_value_length"),
|
|
653
|
+
custom_repr=self.options.get("custom_repr"),
|
|
204
654
|
),
|
|
205
655
|
)
|
|
206
656
|
|
|
207
657
|
before_send = self.options["before_send"]
|
|
208
|
-
if
|
|
658
|
+
if (
|
|
659
|
+
before_send is not None
|
|
660
|
+
and event is not None
|
|
661
|
+
and event.get("type") != "transaction"
|
|
662
|
+
):
|
|
209
663
|
new_event = None
|
|
210
664
|
with capture_internal_exceptions():
|
|
211
665
|
new_event = before_send(event, hint or {})
|
|
212
666
|
if new_event is None:
|
|
213
|
-
logger.info("before send dropped event
|
|
214
|
-
|
|
667
|
+
logger.info("before send dropped event")
|
|
668
|
+
if self.transport:
|
|
669
|
+
self.transport.record_lost_event(
|
|
670
|
+
"before_send", data_category="error"
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# If this is an exception, reset the DedupeIntegration. It still
|
|
674
|
+
# remembers the dropped exception as the last exception, meaning
|
|
675
|
+
# that if the same exception happens again and is not dropped
|
|
676
|
+
# in before_send, it'd get dropped by DedupeIntegration.
|
|
677
|
+
if event.get("exception"):
|
|
678
|
+
DedupeIntegration.reset_last_seen()
|
|
679
|
+
|
|
680
|
+
event = new_event
|
|
681
|
+
|
|
682
|
+
before_send_transaction = self.options["before_send_transaction"]
|
|
683
|
+
if (
|
|
684
|
+
before_send_transaction is not None
|
|
685
|
+
and event is not None
|
|
686
|
+
and event.get("type") == "transaction"
|
|
687
|
+
):
|
|
688
|
+
new_event = None
|
|
689
|
+
spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
|
|
690
|
+
with capture_internal_exceptions():
|
|
691
|
+
new_event = before_send_transaction(event, hint or {})
|
|
692
|
+
if new_event is None:
|
|
693
|
+
logger.info("before send transaction dropped event")
|
|
694
|
+
if self.transport:
|
|
695
|
+
self.transport.record_lost_event(
|
|
696
|
+
reason="before_send", data_category="transaction"
|
|
697
|
+
)
|
|
698
|
+
self.transport.record_lost_event(
|
|
699
|
+
reason="before_send",
|
|
700
|
+
data_category="span",
|
|
701
|
+
quantity=spans_before + 1, # +1 for the transaction itself
|
|
702
|
+
)
|
|
703
|
+
else:
|
|
704
|
+
spans_delta = spans_before - len(new_event.get("spans", []))
|
|
705
|
+
if spans_delta > 0 and self.transport is not None:
|
|
706
|
+
self.transport.record_lost_event(
|
|
707
|
+
reason="before_send", data_category="span", quantity=spans_delta
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
event = new_event
|
|
215
711
|
|
|
216
712
|
return event
|
|
217
713
|
|
|
@@ -221,17 +717,18 @@ class _Client(object):
|
|
|
221
717
|
if exc_info is None:
|
|
222
718
|
return False
|
|
223
719
|
|
|
224
|
-
|
|
225
|
-
|
|
720
|
+
error = exc_info[0]
|
|
721
|
+
error_type_name = get_type_name(exc_info[0])
|
|
722
|
+
error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name)
|
|
226
723
|
|
|
227
|
-
for
|
|
724
|
+
for ignored_error in self.options["ignore_errors"]:
|
|
228
725
|
# String types are matched against the type name in the
|
|
229
726
|
# exception only
|
|
230
|
-
if isinstance(
|
|
231
|
-
if
|
|
727
|
+
if isinstance(ignored_error, str):
|
|
728
|
+
if ignored_error == error_full_name or ignored_error == error_type_name:
|
|
232
729
|
return True
|
|
233
730
|
else:
|
|
234
|
-
if issubclass(
|
|
731
|
+
if issubclass(error, ignored_error):
|
|
235
732
|
return True
|
|
236
733
|
|
|
237
734
|
return False
|
|
@@ -243,20 +740,65 @@ class _Client(object):
|
|
|
243
740
|
scope=None, # type: Optional[Scope]
|
|
244
741
|
):
|
|
245
742
|
# type: (...) -> bool
|
|
246
|
-
|
|
247
|
-
|
|
743
|
+
# Transactions are sampled independent of error events.
|
|
744
|
+
is_transaction = event.get("type") == "transaction"
|
|
745
|
+
if is_transaction:
|
|
248
746
|
return True
|
|
249
747
|
|
|
250
|
-
|
|
748
|
+
ignoring_prevents_recursion = scope is not None and not scope._should_capture
|
|
749
|
+
if ignoring_prevents_recursion:
|
|
251
750
|
return False
|
|
252
751
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
and random.random() >= self.options["sample_rate"]
|
|
256
|
-
):
|
|
752
|
+
ignored_by_config_option = self._is_ignored_error(event, hint)
|
|
753
|
+
if ignored_by_config_option:
|
|
257
754
|
return False
|
|
258
755
|
|
|
259
|
-
|
|
756
|
+
return True
|
|
757
|
+
|
|
758
|
+
def _should_sample_error(
|
|
759
|
+
self,
|
|
760
|
+
event, # type: Event
|
|
761
|
+
hint, # type: Hint
|
|
762
|
+
):
|
|
763
|
+
# type: (...) -> bool
|
|
764
|
+
error_sampler = self.options.get("error_sampler", None)
|
|
765
|
+
|
|
766
|
+
if callable(error_sampler):
|
|
767
|
+
with capture_internal_exceptions():
|
|
768
|
+
sample_rate = error_sampler(event, hint)
|
|
769
|
+
else:
|
|
770
|
+
sample_rate = self.options["sample_rate"]
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate
|
|
774
|
+
except NameError:
|
|
775
|
+
logger.warning(
|
|
776
|
+
"The provided error_sampler raised an error. Defaulting to sampling the event."
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# If the error_sampler raised an error, we should sample the event, since the default behavior
|
|
780
|
+
# (when no sample_rate or error_sampler is provided) is to sample all events.
|
|
781
|
+
not_in_sample_rate = False
|
|
782
|
+
except TypeError:
|
|
783
|
+
parameter, verb = (
|
|
784
|
+
("error_sampler", "returned")
|
|
785
|
+
if callable(error_sampler)
|
|
786
|
+
else ("sample_rate", "contains")
|
|
787
|
+
)
|
|
788
|
+
logger.warning(
|
|
789
|
+
"The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event."
|
|
790
|
+
% (parameter, verb, repr(sample_rate))
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# If the sample_rate has an invalid value, we should sample the event, since the default behavior
|
|
794
|
+
# (when no sample_rate or error_sampler is provided) is to sample all events.
|
|
795
|
+
not_in_sample_rate = False
|
|
796
|
+
|
|
797
|
+
if not_in_sample_rate:
|
|
798
|
+
# because we will not sample this event, record a "lost event".
|
|
799
|
+
if self.transport:
|
|
800
|
+
self.transport.record_lost_event("sample_rate", data_category="error")
|
|
801
|
+
|
|
260
802
|
return False
|
|
261
803
|
|
|
262
804
|
return True
|
|
@@ -276,8 +818,10 @@ class _Client(object):
|
|
|
276
818
|
if exceptions:
|
|
277
819
|
errored = True
|
|
278
820
|
for error in exceptions:
|
|
821
|
+
if isinstance(error, AnnotatedValue):
|
|
822
|
+
error = error.value or {}
|
|
279
823
|
mechanism = error.get("mechanism")
|
|
280
|
-
if mechanism and mechanism.get("handled") is False:
|
|
824
|
+
if isinstance(mechanism, Mapping) and mechanism.get("handled") is False:
|
|
281
825
|
crashed = True
|
|
282
826
|
break
|
|
283
827
|
|
|
@@ -285,7 +829,8 @@ class _Client(object):
|
|
|
285
829
|
|
|
286
830
|
if session.user_agent is None:
|
|
287
831
|
headers = (event.get("request") or {}).get("headers")
|
|
288
|
-
|
|
832
|
+
headers_dict = headers if isinstance(headers, dict) else {}
|
|
833
|
+
for k, v in headers_dict.items():
|
|
289
834
|
if k.lower() == "user-agent":
|
|
290
835
|
user_agent = v
|
|
291
836
|
break
|
|
@@ -310,20 +855,20 @@ class _Client(object):
|
|
|
310
855
|
|
|
311
856
|
:param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object.
|
|
312
857
|
|
|
858
|
+
:param scope: An optional :py:class:`sentry_sdk.Scope` to apply to events.
|
|
859
|
+
|
|
313
860
|
:returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help.
|
|
314
861
|
"""
|
|
315
|
-
|
|
316
|
-
return None
|
|
862
|
+
hint = dict(hint or ()) # type: Hint
|
|
317
863
|
|
|
318
|
-
if self.
|
|
864
|
+
if not self._should_capture(event, hint, scope):
|
|
319
865
|
return None
|
|
320
|
-
|
|
321
|
-
|
|
866
|
+
|
|
867
|
+
profile = event.pop("profile", None)
|
|
868
|
+
|
|
322
869
|
event_id = event.get("event_id")
|
|
323
870
|
if event_id is None:
|
|
324
871
|
event["event_id"] = event_id = uuid.uuid4().hex
|
|
325
|
-
if not self._should_capture(event, hint, scope):
|
|
326
|
-
return None
|
|
327
872
|
event_opt = self._prepare_event(event, hint, scope)
|
|
328
873
|
if event_opt is None:
|
|
329
874
|
return None
|
|
@@ -334,26 +879,189 @@ class _Client(object):
|
|
|
334
879
|
if session:
|
|
335
880
|
self._update_session_from_event(session, event)
|
|
336
881
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
882
|
+
is_transaction = event_opt.get("type") == "transaction"
|
|
883
|
+
is_checkin = event_opt.get("type") == "check_in"
|
|
884
|
+
|
|
885
|
+
if (
|
|
886
|
+
not is_transaction
|
|
887
|
+
and not is_checkin
|
|
888
|
+
and not self._should_sample_error(event, hint)
|
|
889
|
+
):
|
|
890
|
+
return None
|
|
891
|
+
|
|
892
|
+
attachments = hint.get("attachments")
|
|
893
|
+
|
|
894
|
+
trace_context = event_opt.get("contexts", {}).get("trace") or {}
|
|
895
|
+
dynamic_sampling_context = trace_context.pop("dynamic_sampling_context", {})
|
|
896
|
+
|
|
897
|
+
headers = {
|
|
898
|
+
"event_id": event_opt["event_id"],
|
|
899
|
+
"sent_at": format_timestamp(datetime.now(timezone.utc)),
|
|
900
|
+
} # type: dict[str, object]
|
|
901
|
+
|
|
902
|
+
if dynamic_sampling_context:
|
|
903
|
+
headers["trace"] = dynamic_sampling_context
|
|
904
|
+
|
|
905
|
+
envelope = Envelope(headers=headers)
|
|
906
|
+
|
|
907
|
+
if is_transaction:
|
|
908
|
+
if isinstance(profile, Profile):
|
|
909
|
+
envelope.add_profile(profile.to_json(event_opt, self.options))
|
|
910
|
+
envelope.add_transaction(event_opt)
|
|
911
|
+
elif is_checkin:
|
|
912
|
+
envelope.add_checkin(event_opt)
|
|
350
913
|
else:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
914
|
+
envelope.add_event(event_opt)
|
|
915
|
+
|
|
916
|
+
for attachment in attachments or ():
|
|
917
|
+
envelope.add_item(attachment.to_envelope_item())
|
|
918
|
+
|
|
919
|
+
return_value = None
|
|
920
|
+
if self.spotlight:
|
|
921
|
+
self.spotlight.capture_envelope(envelope)
|
|
922
|
+
return_value = event_id
|
|
923
|
+
|
|
924
|
+
if self.transport is not None:
|
|
925
|
+
self.transport.capture_envelope(envelope)
|
|
926
|
+
return_value = event_id
|
|
927
|
+
|
|
928
|
+
return return_value
|
|
929
|
+
|
|
930
|
+
def _capture_log(self, log):
|
|
931
|
+
# type: (Optional[Log]) -> None
|
|
932
|
+
if not has_logs_enabled(self.options) or log is None:
|
|
933
|
+
return
|
|
934
|
+
|
|
935
|
+
current_scope = sentry_sdk.get_current_scope()
|
|
936
|
+
isolation_scope = sentry_sdk.get_isolation_scope()
|
|
937
|
+
|
|
938
|
+
log["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
|
|
939
|
+
log["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]
|
|
940
|
+
|
|
941
|
+
server_name = self.options.get("server_name")
|
|
942
|
+
if server_name is not None and SPANDATA.SERVER_ADDRESS not in log["attributes"]:
|
|
943
|
+
log["attributes"][SPANDATA.SERVER_ADDRESS] = server_name
|
|
944
|
+
|
|
945
|
+
environment = self.options.get("environment")
|
|
946
|
+
if environment is not None and "sentry.environment" not in log["attributes"]:
|
|
947
|
+
log["attributes"]["sentry.environment"] = environment
|
|
948
|
+
|
|
949
|
+
release = self.options.get("release")
|
|
950
|
+
if release is not None and "sentry.release" not in log["attributes"]:
|
|
951
|
+
log["attributes"]["sentry.release"] = release
|
|
952
|
+
|
|
953
|
+
trace_context = current_scope.get_trace_context()
|
|
954
|
+
trace_id = trace_context.get("trace_id")
|
|
955
|
+
span_id = trace_context.get("span_id")
|
|
956
|
+
|
|
957
|
+
if trace_id is not None and log.get("trace_id") is None:
|
|
958
|
+
log["trace_id"] = trace_id
|
|
959
|
+
|
|
960
|
+
if (
|
|
961
|
+
span_id is not None
|
|
962
|
+
and "sentry.trace.parent_span_id" not in log["attributes"]
|
|
963
|
+
):
|
|
964
|
+
log["attributes"]["sentry.trace.parent_span_id"] = span_id
|
|
965
|
+
|
|
966
|
+
# The user, if present, is always set on the isolation scope.
|
|
967
|
+
if isolation_scope._user is not None:
|
|
968
|
+
for log_attribute, user_attribute in (
|
|
969
|
+
("user.id", "id"),
|
|
970
|
+
("user.name", "username"),
|
|
971
|
+
("user.email", "email"),
|
|
972
|
+
):
|
|
973
|
+
if (
|
|
974
|
+
user_attribute in isolation_scope._user
|
|
975
|
+
and log_attribute not in log["attributes"]
|
|
976
|
+
):
|
|
977
|
+
log["attributes"][log_attribute] = isolation_scope._user[
|
|
978
|
+
user_attribute
|
|
979
|
+
]
|
|
980
|
+
|
|
981
|
+
# If debug is enabled, log the log to the console
|
|
982
|
+
debug = self.options.get("debug", False)
|
|
983
|
+
if debug:
|
|
984
|
+
logger.debug(
|
|
985
|
+
f"[Sentry Logs] [{log.get('severity_text')}] {log.get('body')}"
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
before_send_log = get_before_send_log(self.options)
|
|
989
|
+
if before_send_log is not None:
|
|
990
|
+
log = before_send_log(log, {})
|
|
991
|
+
|
|
992
|
+
if log is None:
|
|
993
|
+
return
|
|
994
|
+
|
|
995
|
+
if self.log_batcher:
|
|
996
|
+
self.log_batcher.add(log)
|
|
997
|
+
|
|
998
|
+
def _capture_metric(self, metric):
|
|
999
|
+
# type: (Optional[Metric]) -> None
|
|
1000
|
+
if not has_metrics_enabled(self.options) or metric is None:
|
|
1001
|
+
return
|
|
1002
|
+
|
|
1003
|
+
current_scope = sentry_sdk.get_current_scope()
|
|
1004
|
+
isolation_scope = sentry_sdk.get_isolation_scope()
|
|
1005
|
+
|
|
1006
|
+
metric["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
|
|
1007
|
+
metric["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]
|
|
1008
|
+
|
|
1009
|
+
server_name = self.options.get("server_name")
|
|
1010
|
+
if (
|
|
1011
|
+
server_name is not None
|
|
1012
|
+
and SPANDATA.SERVER_ADDRESS not in metric["attributes"]
|
|
1013
|
+
):
|
|
1014
|
+
metric["attributes"][SPANDATA.SERVER_ADDRESS] = server_name
|
|
1015
|
+
|
|
1016
|
+
environment = self.options.get("environment")
|
|
1017
|
+
if environment is not None and "sentry.environment" not in metric["attributes"]:
|
|
1018
|
+
metric["attributes"]["sentry.environment"] = environment
|
|
1019
|
+
|
|
1020
|
+
release = self.options.get("release")
|
|
1021
|
+
if release is not None and "sentry.release" not in metric["attributes"]:
|
|
1022
|
+
metric["attributes"]["sentry.release"] = release
|
|
1023
|
+
|
|
1024
|
+
trace_context = current_scope.get_trace_context()
|
|
1025
|
+
trace_id = trace_context.get("trace_id")
|
|
1026
|
+
span_id = trace_context.get("span_id")
|
|
1027
|
+
|
|
1028
|
+
metric["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000"
|
|
1029
|
+
if span_id is not None:
|
|
1030
|
+
metric["span_id"] = span_id
|
|
1031
|
+
|
|
1032
|
+
if isolation_scope._user is not None:
|
|
1033
|
+
for metric_attribute, user_attribute in (
|
|
1034
|
+
("user.id", "id"),
|
|
1035
|
+
("user.name", "username"),
|
|
1036
|
+
("user.email", "email"),
|
|
1037
|
+
):
|
|
1038
|
+
if (
|
|
1039
|
+
user_attribute in isolation_scope._user
|
|
1040
|
+
and metric_attribute not in metric["attributes"]
|
|
1041
|
+
):
|
|
1042
|
+
metric["attributes"][metric_attribute] = isolation_scope._user[
|
|
1043
|
+
user_attribute
|
|
1044
|
+
]
|
|
1045
|
+
|
|
1046
|
+
debug = self.options.get("debug", False)
|
|
1047
|
+
if debug:
|
|
1048
|
+
logger.debug(
|
|
1049
|
+
f"[Sentry Metrics] [{metric.get('type')}] {metric.get('name')}: {metric.get('value')}"
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
before_send_metric = get_before_send_metric(self.options)
|
|
1053
|
+
if before_send_metric is not None:
|
|
1054
|
+
metric = before_send_metric(metric, {})
|
|
1055
|
+
|
|
1056
|
+
if metric is None:
|
|
1057
|
+
return
|
|
1058
|
+
|
|
1059
|
+
if self.metrics_batcher:
|
|
1060
|
+
self.metrics_batcher.add(metric)
|
|
354
1061
|
|
|
355
1062
|
def capture_session(
|
|
356
|
-
self,
|
|
1063
|
+
self,
|
|
1064
|
+
session, # type: Session
|
|
357
1065
|
):
|
|
358
1066
|
# type: (...) -> None
|
|
359
1067
|
if not session.release:
|
|
@@ -361,6 +1069,35 @@ class _Client(object):
|
|
|
361
1069
|
else:
|
|
362
1070
|
self.session_flusher.add_session(session)
|
|
363
1071
|
|
|
1072
|
+
if TYPE_CHECKING:
|
|
1073
|
+
|
|
1074
|
+
@overload
|
|
1075
|
+
def get_integration(self, name_or_class):
|
|
1076
|
+
# type: (str) -> Optional[Integration]
|
|
1077
|
+
...
|
|
1078
|
+
|
|
1079
|
+
@overload
|
|
1080
|
+
def get_integration(self, name_or_class):
|
|
1081
|
+
# type: (type[I]) -> Optional[I]
|
|
1082
|
+
...
|
|
1083
|
+
|
|
1084
|
+
def get_integration(
|
|
1085
|
+
self,
|
|
1086
|
+
name_or_class, # type: Union[str, Type[Integration]]
|
|
1087
|
+
):
|
|
1088
|
+
# type: (...) -> Optional[Integration]
|
|
1089
|
+
"""Returns the integration for this client by name or class.
|
|
1090
|
+
If the client does not have that integration then `None` is returned.
|
|
1091
|
+
"""
|
|
1092
|
+
if isinstance(name_or_class, str):
|
|
1093
|
+
integration_name = name_or_class
|
|
1094
|
+
elif name_or_class.identifier is not None:
|
|
1095
|
+
integration_name = name_or_class.identifier
|
|
1096
|
+
else:
|
|
1097
|
+
raise ValueError("Integration has no name")
|
|
1098
|
+
|
|
1099
|
+
return self.integrations.get(integration_name)
|
|
1100
|
+
|
|
364
1101
|
def close(
|
|
365
1102
|
self,
|
|
366
1103
|
timeout=None, # type: Optional[float]
|
|
@@ -374,6 +1111,12 @@ class _Client(object):
|
|
|
374
1111
|
if self.transport is not None:
|
|
375
1112
|
self.flush(timeout=timeout, callback=callback)
|
|
376
1113
|
self.session_flusher.kill()
|
|
1114
|
+
if self.log_batcher is not None:
|
|
1115
|
+
self.log_batcher.kill()
|
|
1116
|
+
if self.metrics_batcher is not None:
|
|
1117
|
+
self.metrics_batcher.kill()
|
|
1118
|
+
if self.monitor:
|
|
1119
|
+
self.monitor.kill()
|
|
377
1120
|
self.transport.kill()
|
|
378
1121
|
self.transport = None
|
|
379
1122
|
|
|
@@ -394,6 +1137,10 @@ class _Client(object):
|
|
|
394
1137
|
if timeout is None:
|
|
395
1138
|
timeout = self.options["shutdown_timeout"]
|
|
396
1139
|
self.session_flusher.flush()
|
|
1140
|
+
if self.log_batcher is not None:
|
|
1141
|
+
self.log_batcher.flush()
|
|
1142
|
+
if self.metrics_batcher is not None:
|
|
1143
|
+
self.metrics_batcher.flush()
|
|
397
1144
|
self.transport.flush(timeout=timeout, callback=callback)
|
|
398
1145
|
|
|
399
1146
|
def __enter__(self):
|
|
@@ -405,9 +1152,9 @@ class _Client(object):
|
|
|
405
1152
|
self.close()
|
|
406
1153
|
|
|
407
1154
|
|
|
408
|
-
from
|
|
1155
|
+
from typing import TYPE_CHECKING
|
|
409
1156
|
|
|
410
|
-
if
|
|
1157
|
+
if TYPE_CHECKING:
|
|
411
1158
|
# Make mypy, PyCharm and other static analyzers think `get_options` is a
|
|
412
1159
|
# type to have nicer autocompletion for params.
|
|
413
1160
|
#
|
|
@@ -420,7 +1167,6 @@ if MYPY:
|
|
|
420
1167
|
class Client(ClientConstructor, _Client):
|
|
421
1168
|
pass
|
|
422
1169
|
|
|
423
|
-
|
|
424
1170
|
else:
|
|
425
1171
|
# Alias `get_options` for actual usage. Go through the lambda indirection
|
|
426
1172
|
# to throw PyCharm off of the weakly typed signature (it would otherwise
|