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/utils.py
CHANGED
|
@@ -1,44 +1,106 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import json
|
|
2
3
|
import linecache
|
|
3
4
|
import logging
|
|
5
|
+
import math
|
|
4
6
|
import os
|
|
7
|
+
import random
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
5
10
|
import sys
|
|
6
|
-
import time
|
|
7
11
|
import threading
|
|
8
|
-
|
|
9
|
-
from
|
|
12
|
+
import time
|
|
13
|
+
from collections import namedtuple
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
from functools import partial, partialmethod, wraps
|
|
17
|
+
from numbers import Real
|
|
18
|
+
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
# Python 3.11
|
|
22
|
+
from builtins import BaseExceptionGroup
|
|
23
|
+
except ImportError:
|
|
24
|
+
# Python 3.10 and below
|
|
25
|
+
BaseExceptionGroup = None # type: ignore
|
|
10
26
|
|
|
11
27
|
import sentry_sdk
|
|
12
|
-
from sentry_sdk._compat import
|
|
28
|
+
from sentry_sdk._compat import PY37
|
|
29
|
+
from sentry_sdk.consts import (
|
|
30
|
+
DEFAULT_ADD_FULL_STACK,
|
|
31
|
+
DEFAULT_MAX_STACK_FRAMES,
|
|
32
|
+
DEFAULT_MAX_VALUE_LENGTH,
|
|
33
|
+
EndpointType,
|
|
34
|
+
)
|
|
35
|
+
from sentry_sdk._types import Annotated, AnnotatedValue, SENSITIVE_DATA_SUBSTITUTE
|
|
36
|
+
|
|
37
|
+
from typing import TYPE_CHECKING
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from types import FrameType, TracebackType
|
|
41
|
+
from typing import (
|
|
42
|
+
Any,
|
|
43
|
+
Callable,
|
|
44
|
+
cast,
|
|
45
|
+
ContextManager,
|
|
46
|
+
Dict,
|
|
47
|
+
Iterator,
|
|
48
|
+
List,
|
|
49
|
+
NoReturn,
|
|
50
|
+
Optional,
|
|
51
|
+
overload,
|
|
52
|
+
ParamSpec,
|
|
53
|
+
Set,
|
|
54
|
+
Tuple,
|
|
55
|
+
Type,
|
|
56
|
+
TypeVar,
|
|
57
|
+
Union,
|
|
58
|
+
)
|
|
13
59
|
|
|
14
|
-
from
|
|
60
|
+
from gevent.hub import Hub
|
|
15
61
|
|
|
16
|
-
|
|
17
|
-
from types import FrameType
|
|
18
|
-
from types import TracebackType
|
|
19
|
-
from typing import Any
|
|
20
|
-
from typing import Callable
|
|
21
|
-
from typing import Dict
|
|
22
|
-
from typing import ContextManager
|
|
23
|
-
from typing import Iterator
|
|
24
|
-
from typing import List
|
|
25
|
-
from typing import Optional
|
|
26
|
-
from typing import Set
|
|
27
|
-
from typing import Tuple
|
|
28
|
-
from typing import Union
|
|
29
|
-
from typing import Type
|
|
62
|
+
from sentry_sdk._types import Event, ExcInfo, Log, Hint, Metric
|
|
30
63
|
|
|
31
|
-
|
|
64
|
+
P = ParamSpec("P")
|
|
65
|
+
R = TypeVar("R")
|
|
32
66
|
|
|
33
67
|
|
|
34
68
|
epoch = datetime(1970, 1, 1)
|
|
35
69
|
|
|
36
|
-
|
|
37
70
|
# The logger is created here but initialized in the debug support module
|
|
38
71
|
logger = logging.getLogger("sentry_sdk.errors")
|
|
39
72
|
|
|
40
|
-
|
|
41
|
-
|
|
73
|
+
_installed_modules = None
|
|
74
|
+
|
|
75
|
+
BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$")
|
|
76
|
+
|
|
77
|
+
FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0"))
|
|
78
|
+
TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1"))
|
|
79
|
+
|
|
80
|
+
MAX_STACK_FRAMES = 2000
|
|
81
|
+
"""Maximum number of stack frames to send to Sentry.
|
|
82
|
+
|
|
83
|
+
If we have more than this number of stack frames, we will stop processing
|
|
84
|
+
the stacktrace to avoid getting stuck in a long-lasting loop. This value
|
|
85
|
+
exceeds the default sys.getrecursionlimit() of 1000, so users will only
|
|
86
|
+
be affected by this limit if they have a custom recursion limit.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def env_to_bool(value, *, strict=False):
|
|
91
|
+
# type: (Any, Optional[bool]) -> bool | None
|
|
92
|
+
"""Casts an ENV variable value to boolean using the constants defined above.
|
|
93
|
+
In strict mode, it may return None if the value doesn't match any of the predefined values.
|
|
94
|
+
"""
|
|
95
|
+
normalized = str(value).lower() if value is not None else None
|
|
96
|
+
|
|
97
|
+
if normalized in FALSY_ENV_VALUES:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
if normalized in TRUTHY_ENV_VALUES:
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
return None if strict else bool(value)
|
|
42
104
|
|
|
43
105
|
|
|
44
106
|
def json_dumps(data):
|
|
@@ -47,13 +109,95 @@ def json_dumps(data):
|
|
|
47
109
|
return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8")
|
|
48
110
|
|
|
49
111
|
|
|
50
|
-
def
|
|
51
|
-
# type: () -> Optional[
|
|
52
|
-
|
|
53
|
-
|
|
112
|
+
def get_git_revision():
|
|
113
|
+
# type: () -> Optional[str]
|
|
114
|
+
try:
|
|
115
|
+
with open(os.path.devnull, "w+") as null:
|
|
116
|
+
# prevent command prompt windows from popping up on windows
|
|
117
|
+
startupinfo = None
|
|
118
|
+
if sys.platform == "win32" or sys.platform == "cygwin":
|
|
119
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
120
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
121
|
+
|
|
122
|
+
revision = (
|
|
123
|
+
subprocess.Popen(
|
|
124
|
+
["git", "rev-parse", "HEAD"],
|
|
125
|
+
startupinfo=startupinfo,
|
|
126
|
+
stdout=subprocess.PIPE,
|
|
127
|
+
stderr=null,
|
|
128
|
+
stdin=null,
|
|
129
|
+
)
|
|
130
|
+
.communicate()[0]
|
|
131
|
+
.strip()
|
|
132
|
+
.decode("utf-8")
|
|
133
|
+
)
|
|
134
|
+
except (OSError, IOError, FileNotFoundError):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
return revision
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_default_release():
|
|
141
|
+
# type: () -> Optional[str]
|
|
142
|
+
"""Try to guess a default release."""
|
|
143
|
+
release = os.environ.get("SENTRY_RELEASE")
|
|
144
|
+
if release:
|
|
145
|
+
return release
|
|
54
146
|
|
|
147
|
+
release = get_git_revision()
|
|
148
|
+
if release:
|
|
149
|
+
return release
|
|
150
|
+
|
|
151
|
+
for var in (
|
|
152
|
+
"HEROKU_SLUG_COMMIT",
|
|
153
|
+
"SOURCE_VERSION",
|
|
154
|
+
"CODEBUILD_RESOLVED_SOURCE_VERSION",
|
|
155
|
+
"CIRCLE_SHA1",
|
|
156
|
+
"GAE_DEPLOYMENT_ID",
|
|
157
|
+
):
|
|
158
|
+
release = os.environ.get(var)
|
|
159
|
+
if release:
|
|
160
|
+
return release
|
|
161
|
+
return None
|
|
55
162
|
|
|
56
|
-
|
|
163
|
+
|
|
164
|
+
def get_sdk_name(installed_integrations):
|
|
165
|
+
# type: (List[str]) -> str
|
|
166
|
+
"""Return the SDK name including the name of the used web framework."""
|
|
167
|
+
|
|
168
|
+
# Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier
|
|
169
|
+
# here because if django is not installed the integration is not accessible.
|
|
170
|
+
framework_integrations = [
|
|
171
|
+
"django",
|
|
172
|
+
"flask",
|
|
173
|
+
"fastapi",
|
|
174
|
+
"bottle",
|
|
175
|
+
"falcon",
|
|
176
|
+
"quart",
|
|
177
|
+
"sanic",
|
|
178
|
+
"starlette",
|
|
179
|
+
"litestar",
|
|
180
|
+
"starlite",
|
|
181
|
+
"chalice",
|
|
182
|
+
"serverless",
|
|
183
|
+
"pyramid",
|
|
184
|
+
"tornado",
|
|
185
|
+
"aiohttp",
|
|
186
|
+
"aws_lambda",
|
|
187
|
+
"gcp",
|
|
188
|
+
"beam",
|
|
189
|
+
"asgi",
|
|
190
|
+
"wsgi",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
for integration in framework_integrations:
|
|
194
|
+
if integration in installed_integrations:
|
|
195
|
+
return "sentry.python.{}".format(integration)
|
|
196
|
+
|
|
197
|
+
return "sentry.python"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CaptureInternalException:
|
|
57
201
|
__slots__ = ()
|
|
58
202
|
|
|
59
203
|
def __enter__(self):
|
|
@@ -78,9 +222,14 @@ def capture_internal_exceptions():
|
|
|
78
222
|
|
|
79
223
|
def capture_internal_exception(exc_info):
|
|
80
224
|
# type: (ExcInfo) -> None
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
225
|
+
"""
|
|
226
|
+
Capture an exception that is likely caused by a bug in the SDK
|
|
227
|
+
itself.
|
|
228
|
+
|
|
229
|
+
These exceptions do not end up in Sentry and are just logged instead.
|
|
230
|
+
"""
|
|
231
|
+
if sentry_sdk.get_client().is_active():
|
|
232
|
+
logger.error("Internal error in sentry_sdk", exc_info=exc_info)
|
|
84
233
|
|
|
85
234
|
|
|
86
235
|
def to_timestamp(value):
|
|
@@ -90,7 +239,40 @@ def to_timestamp(value):
|
|
|
90
239
|
|
|
91
240
|
def format_timestamp(value):
|
|
92
241
|
# type: (datetime) -> str
|
|
93
|
-
|
|
242
|
+
"""Formats a timestamp in RFC 3339 format.
|
|
243
|
+
|
|
244
|
+
Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC.
|
|
245
|
+
"""
|
|
246
|
+
utctime = value.astimezone(timezone.utc)
|
|
247
|
+
|
|
248
|
+
# We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for
|
|
249
|
+
# several years now), and isoformat is slightly different.
|
|
250
|
+
return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
ISO_TZ_SEPARATORS = frozenset(("+", "-"))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def datetime_from_isoformat(value):
|
|
257
|
+
# type: (str) -> datetime
|
|
258
|
+
try:
|
|
259
|
+
result = datetime.fromisoformat(value)
|
|
260
|
+
except (AttributeError, ValueError):
|
|
261
|
+
# py 3.6
|
|
262
|
+
timestamp_format = (
|
|
263
|
+
"%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S"
|
|
264
|
+
)
|
|
265
|
+
if value.endswith("Z"):
|
|
266
|
+
value = value[:-1] + "+0000"
|
|
267
|
+
|
|
268
|
+
if value[-6] in ISO_TZ_SEPARATORS:
|
|
269
|
+
timestamp_format += "%z"
|
|
270
|
+
value = value[:-3] + value[-2:]
|
|
271
|
+
elif value[-5] in ISO_TZ_SEPARATORS:
|
|
272
|
+
timestamp_format += "%z"
|
|
273
|
+
|
|
274
|
+
result = datetime.strptime(value, timestamp_format)
|
|
275
|
+
return result.astimezone(timezone.utc)
|
|
94
276
|
|
|
95
277
|
|
|
96
278
|
def event_hint_with_exc_info(exc_info=None):
|
|
@@ -109,8 +291,7 @@ class BadDsn(ValueError):
|
|
|
109
291
|
"""Raised on invalid DSNs."""
|
|
110
292
|
|
|
111
293
|
|
|
112
|
-
|
|
113
|
-
class Dsn(object):
|
|
294
|
+
class Dsn:
|
|
114
295
|
"""Represents a DSN."""
|
|
115
296
|
|
|
116
297
|
def __init__(self, value):
|
|
@@ -118,9 +299,9 @@ class Dsn(object):
|
|
|
118
299
|
if isinstance(value, Dsn):
|
|
119
300
|
self.__dict__ = dict(value.__dict__)
|
|
120
301
|
return
|
|
121
|
-
parts =
|
|
302
|
+
parts = urlsplit(str(value))
|
|
122
303
|
|
|
123
|
-
if parts.scheme not in (
|
|
304
|
+
if parts.scheme not in ("http", "https"):
|
|
124
305
|
raise BadDsn("Unsupported scheme %r" % parts.scheme)
|
|
125
306
|
self.scheme = parts.scheme
|
|
126
307
|
|
|
@@ -130,7 +311,7 @@ class Dsn(object):
|
|
|
130
311
|
self.host = parts.hostname
|
|
131
312
|
|
|
132
313
|
if parts.port is None:
|
|
133
|
-
self.port = self.scheme == "https" and 443 or 80
|
|
314
|
+
self.port = self.scheme == "https" and 443 or 80 # type: int
|
|
134
315
|
else:
|
|
135
316
|
self.port = parts.port
|
|
136
317
|
|
|
@@ -143,7 +324,7 @@ class Dsn(object):
|
|
|
143
324
|
path = parts.path.rsplit("/", 1)
|
|
144
325
|
|
|
145
326
|
try:
|
|
146
|
-
self.project_id =
|
|
327
|
+
self.project_id = str(int(path.pop()))
|
|
147
328
|
except (ValueError, TypeError):
|
|
148
329
|
raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:])
|
|
149
330
|
|
|
@@ -183,7 +364,7 @@ class Dsn(object):
|
|
|
183
364
|
)
|
|
184
365
|
|
|
185
366
|
|
|
186
|
-
class Auth
|
|
367
|
+
class Auth:
|
|
187
368
|
"""Helper object that represents the auth info."""
|
|
188
369
|
|
|
189
370
|
def __init__(
|
|
@@ -207,17 +388,9 @@ class Auth(object):
|
|
|
207
388
|
self.version = version
|
|
208
389
|
self.client = client
|
|
209
390
|
|
|
210
|
-
@property
|
|
211
|
-
def store_api_url(self):
|
|
212
|
-
# type: () -> str
|
|
213
|
-
"""Returns the API url for storing events.
|
|
214
|
-
|
|
215
|
-
Deprecated: use get_api_url instead.
|
|
216
|
-
"""
|
|
217
|
-
return self.get_api_url(type="store")
|
|
218
|
-
|
|
219
391
|
def get_api_url(
|
|
220
|
-
self,
|
|
392
|
+
self,
|
|
393
|
+
type=EndpointType.ENVELOPE, # type: EndpointType
|
|
221
394
|
):
|
|
222
395
|
# type: (...) -> str
|
|
223
396
|
"""Returns the API url for storing events."""
|
|
@@ -226,36 +399,18 @@ class Auth(object):
|
|
|
226
399
|
self.host,
|
|
227
400
|
self.path,
|
|
228
401
|
self.project_id,
|
|
229
|
-
type,
|
|
402
|
+
type.value,
|
|
230
403
|
)
|
|
231
404
|
|
|
232
|
-
def to_header(self
|
|
233
|
-
# type: (
|
|
405
|
+
def to_header(self):
|
|
406
|
+
# type: () -> str
|
|
234
407
|
"""Returns the auth header a string."""
|
|
235
408
|
rv = [("sentry_key", self.public_key), ("sentry_version", self.version)]
|
|
236
|
-
if timestamp is not None:
|
|
237
|
-
rv.append(("sentry_timestamp", str(to_timestamp(timestamp))))
|
|
238
409
|
if self.client is not None:
|
|
239
410
|
rv.append(("sentry_client", self.client))
|
|
240
411
|
if self.secret_key is not None:
|
|
241
412
|
rv.append(("sentry_secret", self.secret_key))
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
class AnnotatedValue(object):
|
|
246
|
-
__slots__ = ("value", "metadata")
|
|
247
|
-
|
|
248
|
-
def __init__(self, value, metadata):
|
|
249
|
-
# type: (Optional[Any], Dict[str, Any]) -> None
|
|
250
|
-
self.value = value
|
|
251
|
-
self.metadata = metadata
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if MYPY:
|
|
255
|
-
from typing import TypeVar
|
|
256
|
-
|
|
257
|
-
T = TypeVar("T")
|
|
258
|
-
Annotated = Union[AnnotatedValue, T]
|
|
413
|
+
return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv)
|
|
259
414
|
|
|
260
415
|
|
|
261
416
|
def get_type_name(cls):
|
|
@@ -302,6 +457,7 @@ def iter_stacks(tb):
|
|
|
302
457
|
def get_lines_from_file(
|
|
303
458
|
filename, # type: str
|
|
304
459
|
lineno, # type: int
|
|
460
|
+
max_length=None, # type: Optional[int]
|
|
305
461
|
loader=None, # type: Optional[Any]
|
|
306
462
|
module=None, # type: Optional[str]
|
|
307
463
|
):
|
|
@@ -330,11 +486,12 @@ def get_lines_from_file(
|
|
|
330
486
|
|
|
331
487
|
try:
|
|
332
488
|
pre_context = [
|
|
333
|
-
strip_string(line.strip("\r\n"))
|
|
489
|
+
strip_string(line.strip("\r\n"), max_length=max_length)
|
|
490
|
+
for line in source[lower_bound:lineno]
|
|
334
491
|
]
|
|
335
|
-
context_line = strip_string(source[lineno].strip("\r\n"))
|
|
492
|
+
context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length)
|
|
336
493
|
post_context = [
|
|
337
|
-
strip_string(line.strip("\r\n"))
|
|
494
|
+
strip_string(line.strip("\r\n"), max_length=max_length)
|
|
338
495
|
for line in source[(lineno + 1) : upper_bound]
|
|
339
496
|
]
|
|
340
497
|
return pre_context, context_line, post_context
|
|
@@ -345,7 +502,8 @@ def get_lines_from_file(
|
|
|
345
502
|
|
|
346
503
|
def get_source_context(
|
|
347
504
|
frame, # type: FrameType
|
|
348
|
-
tb_lineno, # type: int
|
|
505
|
+
tb_lineno, # type: Optional[int]
|
|
506
|
+
max_value_length=None, # type: Optional[int]
|
|
349
507
|
):
|
|
350
508
|
# type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
|
|
351
509
|
try:
|
|
@@ -360,56 +518,30 @@ def get_source_context(
|
|
|
360
518
|
loader = frame.f_globals["__loader__"]
|
|
361
519
|
except Exception:
|
|
362
520
|
loader = None
|
|
363
|
-
|
|
364
|
-
if
|
|
365
|
-
|
|
521
|
+
|
|
522
|
+
if tb_lineno is not None and abs_path:
|
|
523
|
+
lineno = tb_lineno - 1
|
|
524
|
+
return get_lines_from_file(
|
|
525
|
+
abs_path, lineno, max_value_length, loader=loader, module=module
|
|
526
|
+
)
|
|
527
|
+
|
|
366
528
|
return [], None, []
|
|
367
529
|
|
|
368
530
|
|
|
369
531
|
def safe_str(value):
|
|
370
532
|
# type: (Any) -> str
|
|
371
533
|
try:
|
|
372
|
-
return
|
|
534
|
+
return str(value)
|
|
373
535
|
except Exception:
|
|
374
536
|
return safe_repr(value)
|
|
375
537
|
|
|
376
538
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
# At this point `rv` contains a bunch of literal escape codes, like
|
|
385
|
-
# this (exaggerated example):
|
|
386
|
-
#
|
|
387
|
-
# u"\\x2f"
|
|
388
|
-
#
|
|
389
|
-
# But we want to show this string as:
|
|
390
|
-
#
|
|
391
|
-
# u"/"
|
|
392
|
-
try:
|
|
393
|
-
# unicode-escape does this job, but can only decode latin1. So we
|
|
394
|
-
# attempt to encode in latin1.
|
|
395
|
-
return rv.encode("latin1").decode("unicode-escape")
|
|
396
|
-
except Exception:
|
|
397
|
-
# Since usually strings aren't latin1 this can break. In those
|
|
398
|
-
# cases we just give up.
|
|
399
|
-
return rv
|
|
400
|
-
except Exception:
|
|
401
|
-
# If e.g. the call to `repr` already fails
|
|
402
|
-
return u"<broken repr>"
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
else:
|
|
406
|
-
|
|
407
|
-
def safe_repr(value):
|
|
408
|
-
# type: (Any) -> str
|
|
409
|
-
try:
|
|
410
|
-
return repr(value)
|
|
411
|
-
except Exception:
|
|
412
|
-
return "<broken repr>"
|
|
539
|
+
def safe_repr(value):
|
|
540
|
+
# type: (Any) -> str
|
|
541
|
+
try:
|
|
542
|
+
return repr(value)
|
|
543
|
+
except Exception:
|
|
544
|
+
return "<broken repr>"
|
|
413
545
|
|
|
414
546
|
|
|
415
547
|
def filename_for_module(module, abs_path):
|
|
@@ -426,6 +558,9 @@ def filename_for_module(module, abs_path):
|
|
|
426
558
|
return os.path.basename(abs_path)
|
|
427
559
|
|
|
428
560
|
base_module_path = sys.modules[base_module].__file__
|
|
561
|
+
if not base_module_path:
|
|
562
|
+
return abs_path
|
|
563
|
+
|
|
429
564
|
return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip(
|
|
430
565
|
os.sep
|
|
431
566
|
)
|
|
@@ -433,8 +568,15 @@ def filename_for_module(module, abs_path):
|
|
|
433
568
|
return abs_path
|
|
434
569
|
|
|
435
570
|
|
|
436
|
-
def serialize_frame(
|
|
437
|
-
|
|
571
|
+
def serialize_frame(
|
|
572
|
+
frame,
|
|
573
|
+
tb_lineno=None,
|
|
574
|
+
include_local_variables=True,
|
|
575
|
+
include_source_context=True,
|
|
576
|
+
max_value_length=None,
|
|
577
|
+
custom_repr=None,
|
|
578
|
+
):
|
|
579
|
+
# type: (FrameType, Optional[int], bool, bool, Optional[int], Optional[Callable[..., Optional[str]]]) -> Dict[str, Any]
|
|
438
580
|
f_code = getattr(frame, "f_code", None)
|
|
439
581
|
if not f_code:
|
|
440
582
|
abs_path = None
|
|
@@ -450,33 +592,54 @@ def serialize_frame(frame, tb_lineno=None, with_locals=True):
|
|
|
450
592
|
if tb_lineno is None:
|
|
451
593
|
tb_lineno = frame.f_lineno
|
|
452
594
|
|
|
453
|
-
|
|
595
|
+
try:
|
|
596
|
+
os_abs_path = os.path.abspath(abs_path) if abs_path else None
|
|
597
|
+
except Exception:
|
|
598
|
+
os_abs_path = None
|
|
454
599
|
|
|
455
600
|
rv = {
|
|
456
601
|
"filename": filename_for_module(module, abs_path) or None,
|
|
457
|
-
"abs_path":
|
|
602
|
+
"abs_path": os_abs_path,
|
|
458
603
|
"function": function or "<unknown>",
|
|
459
604
|
"module": module,
|
|
460
605
|
"lineno": tb_lineno,
|
|
461
|
-
"pre_context": pre_context,
|
|
462
|
-
"context_line": context_line,
|
|
463
|
-
"post_context": post_context,
|
|
464
606
|
} # type: Dict[str, Any]
|
|
465
|
-
|
|
466
|
-
|
|
607
|
+
|
|
608
|
+
if include_source_context:
|
|
609
|
+
rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context(
|
|
610
|
+
frame, tb_lineno, max_value_length
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if include_local_variables:
|
|
614
|
+
from sentry_sdk.serializer import serialize
|
|
615
|
+
|
|
616
|
+
rv["vars"] = serialize(
|
|
617
|
+
dict(frame.f_locals), is_vars=True, custom_repr=custom_repr
|
|
618
|
+
)
|
|
467
619
|
|
|
468
620
|
return rv
|
|
469
621
|
|
|
470
622
|
|
|
471
|
-
def current_stacktrace(
|
|
472
|
-
# type:
|
|
623
|
+
def current_stacktrace(
|
|
624
|
+
include_local_variables=True, # type: bool
|
|
625
|
+
include_source_context=True, # type: bool
|
|
626
|
+
max_value_length=None, # type: Optional[int]
|
|
627
|
+
):
|
|
628
|
+
# type: (...) -> Dict[str, Any]
|
|
473
629
|
__tracebackhide__ = True
|
|
474
630
|
frames = []
|
|
475
631
|
|
|
476
632
|
f = sys._getframe() # type: Optional[FrameType]
|
|
477
633
|
while f is not None:
|
|
478
634
|
if not should_hide_frame(f):
|
|
479
|
-
frames.append(
|
|
635
|
+
frames.append(
|
|
636
|
+
serialize_frame(
|
|
637
|
+
f,
|
|
638
|
+
include_local_variables=include_local_variables,
|
|
639
|
+
include_source_context=include_source_context,
|
|
640
|
+
max_value_length=max_value_length,
|
|
641
|
+
)
|
|
642
|
+
)
|
|
480
643
|
f = f.f_back
|
|
481
644
|
|
|
482
645
|
frames.reverse()
|
|
@@ -489,46 +652,126 @@ def get_errno(exc_value):
|
|
|
489
652
|
return getattr(exc_value, "errno", None)
|
|
490
653
|
|
|
491
654
|
|
|
655
|
+
def get_error_message(exc_value):
|
|
656
|
+
# type: (Optional[BaseException]) -> str
|
|
657
|
+
message = safe_str(
|
|
658
|
+
getattr(exc_value, "message", "")
|
|
659
|
+
or getattr(exc_value, "detail", "")
|
|
660
|
+
or safe_str(exc_value)
|
|
661
|
+
) # type: str
|
|
662
|
+
|
|
663
|
+
# __notes__ should be a list of strings when notes are added
|
|
664
|
+
# via add_note, but can be anything else if __notes__ is set
|
|
665
|
+
# directly. We only support strings in __notes__, since that
|
|
666
|
+
# is the correct use.
|
|
667
|
+
notes = getattr(exc_value, "__notes__", None) # type: object
|
|
668
|
+
if isinstance(notes, list) and len(notes) > 0:
|
|
669
|
+
message += "\n" + "\n".join(note for note in notes if isinstance(note, str))
|
|
670
|
+
|
|
671
|
+
return message
|
|
672
|
+
|
|
673
|
+
|
|
492
674
|
def single_exception_from_error_tuple(
|
|
493
675
|
exc_type, # type: Optional[type]
|
|
494
676
|
exc_value, # type: Optional[BaseException]
|
|
495
677
|
tb, # type: Optional[TracebackType]
|
|
496
678
|
client_options=None, # type: Optional[Dict[str, Any]]
|
|
497
679
|
mechanism=None, # type: Optional[Dict[str, Any]]
|
|
680
|
+
exception_id=None, # type: Optional[int]
|
|
681
|
+
parent_id=None, # type: Optional[int]
|
|
682
|
+
source=None, # type: Optional[str]
|
|
683
|
+
full_stack=None, # type: Optional[list[dict[str, Any]]]
|
|
498
684
|
):
|
|
499
685
|
# type: (...) -> Dict[str, Any]
|
|
686
|
+
"""
|
|
687
|
+
Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry.
|
|
688
|
+
|
|
689
|
+
See the Exception Interface documentation for more details:
|
|
690
|
+
https://develop.sentry.dev/sdk/event-payloads/exception/
|
|
691
|
+
"""
|
|
692
|
+
exception_value = {} # type: Dict[str, Any]
|
|
693
|
+
exception_value["mechanism"] = (
|
|
694
|
+
mechanism.copy() if mechanism else {"type": "generic", "handled": True}
|
|
695
|
+
)
|
|
696
|
+
if exception_id is not None:
|
|
697
|
+
exception_value["mechanism"]["exception_id"] = exception_id
|
|
698
|
+
|
|
500
699
|
if exc_value is not None:
|
|
501
700
|
errno = get_errno(exc_value)
|
|
502
701
|
else:
|
|
503
702
|
errno = None
|
|
504
703
|
|
|
505
704
|
if errno is not None:
|
|
506
|
-
mechanism
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
705
|
+
exception_value["mechanism"].setdefault("meta", {}).setdefault(
|
|
706
|
+
"errno", {}
|
|
707
|
+
).setdefault("number", errno)
|
|
708
|
+
|
|
709
|
+
if source is not None:
|
|
710
|
+
exception_value["mechanism"]["source"] = source
|
|
711
|
+
|
|
712
|
+
is_root_exception = exception_id == 0
|
|
713
|
+
if not is_root_exception and parent_id is not None:
|
|
714
|
+
exception_value["mechanism"]["parent_id"] = parent_id
|
|
715
|
+
exception_value["mechanism"]["type"] = "chained"
|
|
716
|
+
|
|
717
|
+
if is_root_exception and "type" not in exception_value["mechanism"]:
|
|
718
|
+
exception_value["mechanism"]["type"] = "generic"
|
|
719
|
+
|
|
720
|
+
is_exception_group = BaseExceptionGroup is not None and isinstance(
|
|
721
|
+
exc_value, BaseExceptionGroup
|
|
722
|
+
)
|
|
723
|
+
if is_exception_group:
|
|
724
|
+
exception_value["mechanism"]["is_exception_group"] = True
|
|
725
|
+
|
|
726
|
+
exception_value["module"] = get_type_module(exc_type)
|
|
727
|
+
exception_value["type"] = get_type_name(exc_type)
|
|
728
|
+
exception_value["value"] = get_error_message(exc_value)
|
|
510
729
|
|
|
511
730
|
if client_options is None:
|
|
512
|
-
|
|
731
|
+
include_local_variables = True
|
|
732
|
+
include_source_context = True
|
|
733
|
+
max_value_length = DEFAULT_MAX_VALUE_LENGTH # fallback
|
|
734
|
+
custom_repr = None
|
|
513
735
|
else:
|
|
514
|
-
|
|
736
|
+
include_local_variables = client_options["include_local_variables"]
|
|
737
|
+
include_source_context = client_options["include_source_context"]
|
|
738
|
+
max_value_length = client_options["max_value_length"]
|
|
739
|
+
custom_repr = client_options.get("custom_repr")
|
|
515
740
|
|
|
516
741
|
frames = [
|
|
517
|
-
serialize_frame(
|
|
518
|
-
|
|
519
|
-
|
|
742
|
+
serialize_frame(
|
|
743
|
+
tb.tb_frame,
|
|
744
|
+
tb_lineno=tb.tb_lineno,
|
|
745
|
+
include_local_variables=include_local_variables,
|
|
746
|
+
include_source_context=include_source_context,
|
|
747
|
+
max_value_length=max_value_length,
|
|
748
|
+
custom_repr=custom_repr,
|
|
749
|
+
)
|
|
750
|
+
# Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on
|
|
751
|
+
# processing a super-long stacktrace.
|
|
752
|
+
for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1))
|
|
753
|
+
] # type: List[Dict[str, Any]]
|
|
754
|
+
|
|
755
|
+
if len(frames) > MAX_STACK_FRAMES:
|
|
756
|
+
# If we have more frames than the limit, we remove the stacktrace completely.
|
|
757
|
+
# We don't trim the stacktrace here because we have not processed the whole
|
|
758
|
+
# thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would
|
|
759
|
+
# intelligently trim by removing frames in the middle of the stacktrace, but
|
|
760
|
+
# since we don't have the whole stacktrace, we can't do that. Instead, we
|
|
761
|
+
# drop the entire stacktrace.
|
|
762
|
+
exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit(
|
|
763
|
+
value=None
|
|
764
|
+
)
|
|
520
765
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
766
|
+
elif frames:
|
|
767
|
+
if not full_stack:
|
|
768
|
+
new_frames = frames
|
|
769
|
+
else:
|
|
770
|
+
new_frames = merge_stack_frames(frames, full_stack, client_options)
|
|
527
771
|
|
|
528
|
-
|
|
529
|
-
rv["stacktrace"] = {"frames": frames}
|
|
772
|
+
exception_value["stacktrace"] = {"frames": new_frames}
|
|
530
773
|
|
|
531
|
-
return
|
|
774
|
+
return exception_value
|
|
532
775
|
|
|
533
776
|
|
|
534
777
|
HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__")
|
|
@@ -565,7 +808,6 @@ if HAS_CHAINED_EXCEPTIONS:
|
|
|
565
808
|
exc_value = cause
|
|
566
809
|
tb = getattr(cause, "__traceback__", None)
|
|
567
810
|
|
|
568
|
-
|
|
569
811
|
else:
|
|
570
812
|
|
|
571
813
|
def walk_exception_chain(exc_info):
|
|
@@ -573,36 +815,163 @@ else:
|
|
|
573
815
|
yield exc_info
|
|
574
816
|
|
|
575
817
|
|
|
818
|
+
def exceptions_from_error(
|
|
819
|
+
exc_type, # type: Optional[type]
|
|
820
|
+
exc_value, # type: Optional[BaseException]
|
|
821
|
+
tb, # type: Optional[TracebackType]
|
|
822
|
+
client_options=None, # type: Optional[Dict[str, Any]]
|
|
823
|
+
mechanism=None, # type: Optional[Dict[str, Any]]
|
|
824
|
+
exception_id=0, # type: int
|
|
825
|
+
parent_id=0, # type: int
|
|
826
|
+
source=None, # type: Optional[str]
|
|
827
|
+
full_stack=None, # type: Optional[list[dict[str, Any]]]
|
|
828
|
+
):
|
|
829
|
+
# type: (...) -> Tuple[int, List[Dict[str, Any]]]
|
|
830
|
+
"""
|
|
831
|
+
Creates the list of exceptions.
|
|
832
|
+
This can include chained exceptions and exceptions from an ExceptionGroup.
|
|
833
|
+
|
|
834
|
+
See the Exception Interface documentation for more details:
|
|
835
|
+
https://develop.sentry.dev/sdk/event-payloads/exception/
|
|
836
|
+
"""
|
|
837
|
+
|
|
838
|
+
parent = single_exception_from_error_tuple(
|
|
839
|
+
exc_type=exc_type,
|
|
840
|
+
exc_value=exc_value,
|
|
841
|
+
tb=tb,
|
|
842
|
+
client_options=client_options,
|
|
843
|
+
mechanism=mechanism,
|
|
844
|
+
exception_id=exception_id,
|
|
845
|
+
parent_id=parent_id,
|
|
846
|
+
source=source,
|
|
847
|
+
full_stack=full_stack,
|
|
848
|
+
)
|
|
849
|
+
exceptions = [parent]
|
|
850
|
+
|
|
851
|
+
parent_id = exception_id
|
|
852
|
+
exception_id += 1
|
|
853
|
+
|
|
854
|
+
should_supress_context = (
|
|
855
|
+
hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore
|
|
856
|
+
)
|
|
857
|
+
if should_supress_context:
|
|
858
|
+
# Add direct cause.
|
|
859
|
+
# The field `__cause__` is set when raised with the exception (using the `from` keyword).
|
|
860
|
+
exception_has_cause = (
|
|
861
|
+
exc_value
|
|
862
|
+
and hasattr(exc_value, "__cause__")
|
|
863
|
+
and exc_value.__cause__ is not None
|
|
864
|
+
)
|
|
865
|
+
if exception_has_cause:
|
|
866
|
+
cause = exc_value.__cause__ # type: ignore
|
|
867
|
+
(exception_id, child_exceptions) = exceptions_from_error(
|
|
868
|
+
exc_type=type(cause),
|
|
869
|
+
exc_value=cause,
|
|
870
|
+
tb=getattr(cause, "__traceback__", None),
|
|
871
|
+
client_options=client_options,
|
|
872
|
+
mechanism=mechanism,
|
|
873
|
+
exception_id=exception_id,
|
|
874
|
+
source="__cause__",
|
|
875
|
+
full_stack=full_stack,
|
|
876
|
+
)
|
|
877
|
+
exceptions.extend(child_exceptions)
|
|
878
|
+
|
|
879
|
+
else:
|
|
880
|
+
# Add indirect cause.
|
|
881
|
+
# The field `__context__` is assigned if another exception occurs while handling the exception.
|
|
882
|
+
exception_has_content = (
|
|
883
|
+
exc_value
|
|
884
|
+
and hasattr(exc_value, "__context__")
|
|
885
|
+
and exc_value.__context__ is not None
|
|
886
|
+
)
|
|
887
|
+
if exception_has_content:
|
|
888
|
+
context = exc_value.__context__ # type: ignore
|
|
889
|
+
(exception_id, child_exceptions) = exceptions_from_error(
|
|
890
|
+
exc_type=type(context),
|
|
891
|
+
exc_value=context,
|
|
892
|
+
tb=getattr(context, "__traceback__", None),
|
|
893
|
+
client_options=client_options,
|
|
894
|
+
mechanism=mechanism,
|
|
895
|
+
exception_id=exception_id,
|
|
896
|
+
source="__context__",
|
|
897
|
+
full_stack=full_stack,
|
|
898
|
+
)
|
|
899
|
+
exceptions.extend(child_exceptions)
|
|
900
|
+
|
|
901
|
+
# Add exceptions from an ExceptionGroup.
|
|
902
|
+
is_exception_group = exc_value and hasattr(exc_value, "exceptions")
|
|
903
|
+
if is_exception_group:
|
|
904
|
+
for idx, e in enumerate(exc_value.exceptions): # type: ignore
|
|
905
|
+
(exception_id, child_exceptions) = exceptions_from_error(
|
|
906
|
+
exc_type=type(e),
|
|
907
|
+
exc_value=e,
|
|
908
|
+
tb=getattr(e, "__traceback__", None),
|
|
909
|
+
client_options=client_options,
|
|
910
|
+
mechanism=mechanism,
|
|
911
|
+
exception_id=exception_id,
|
|
912
|
+
parent_id=parent_id,
|
|
913
|
+
source="exceptions[%s]" % idx,
|
|
914
|
+
full_stack=full_stack,
|
|
915
|
+
)
|
|
916
|
+
exceptions.extend(child_exceptions)
|
|
917
|
+
|
|
918
|
+
return (exception_id, exceptions)
|
|
919
|
+
|
|
920
|
+
|
|
576
921
|
def exceptions_from_error_tuple(
|
|
577
922
|
exc_info, # type: ExcInfo
|
|
578
923
|
client_options=None, # type: Optional[Dict[str, Any]]
|
|
579
924
|
mechanism=None, # type: Optional[Dict[str, Any]]
|
|
925
|
+
full_stack=None, # type: Optional[list[dict[str, Any]]]
|
|
580
926
|
):
|
|
581
927
|
# type: (...) -> List[Dict[str, Any]]
|
|
582
928
|
exc_type, exc_value, tb = exc_info
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
929
|
+
|
|
930
|
+
is_exception_group = BaseExceptionGroup is not None and isinstance(
|
|
931
|
+
exc_value, BaseExceptionGroup
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
if is_exception_group:
|
|
935
|
+
(_, exceptions) = exceptions_from_error(
|
|
936
|
+
exc_type=exc_type,
|
|
937
|
+
exc_value=exc_value,
|
|
938
|
+
tb=tb,
|
|
939
|
+
client_options=client_options,
|
|
940
|
+
mechanism=mechanism,
|
|
941
|
+
exception_id=0,
|
|
942
|
+
parent_id=0,
|
|
943
|
+
full_stack=full_stack,
|
|
589
944
|
)
|
|
590
945
|
|
|
591
|
-
|
|
946
|
+
else:
|
|
947
|
+
exceptions = []
|
|
948
|
+
for exc_type, exc_value, tb in walk_exception_chain(exc_info):
|
|
949
|
+
exceptions.append(
|
|
950
|
+
single_exception_from_error_tuple(
|
|
951
|
+
exc_type=exc_type,
|
|
952
|
+
exc_value=exc_value,
|
|
953
|
+
tb=tb,
|
|
954
|
+
client_options=client_options,
|
|
955
|
+
mechanism=mechanism,
|
|
956
|
+
full_stack=full_stack,
|
|
957
|
+
)
|
|
958
|
+
)
|
|
592
959
|
|
|
593
|
-
|
|
960
|
+
exceptions.reverse()
|
|
961
|
+
|
|
962
|
+
return exceptions
|
|
594
963
|
|
|
595
964
|
|
|
596
965
|
def to_string(value):
|
|
597
966
|
# type: (str) -> str
|
|
598
967
|
try:
|
|
599
|
-
return
|
|
968
|
+
return str(value)
|
|
600
969
|
except UnicodeDecodeError:
|
|
601
970
|
return repr(value)[1:-1]
|
|
602
971
|
|
|
603
972
|
|
|
604
973
|
def iter_event_stacktraces(event):
|
|
605
|
-
# type: (
|
|
974
|
+
# type: (Event) -> Iterator[Annotated[Dict[str, Any]]]
|
|
606
975
|
if "stacktrace" in event:
|
|
607
976
|
yield event["stacktrace"]
|
|
608
977
|
if "threads" in event:
|
|
@@ -611,55 +980,71 @@ def iter_event_stacktraces(event):
|
|
|
611
980
|
yield thread["stacktrace"]
|
|
612
981
|
if "exception" in event:
|
|
613
982
|
for exception in event["exception"].get("values") or ():
|
|
614
|
-
if "stacktrace" in exception:
|
|
983
|
+
if isinstance(exception, dict) and "stacktrace" in exception:
|
|
615
984
|
yield exception["stacktrace"]
|
|
616
985
|
|
|
617
986
|
|
|
618
987
|
def iter_event_frames(event):
|
|
619
|
-
# type: (
|
|
988
|
+
# type: (Event) -> Iterator[Dict[str, Any]]
|
|
620
989
|
for stacktrace in iter_event_stacktraces(event):
|
|
990
|
+
if isinstance(stacktrace, AnnotatedValue):
|
|
991
|
+
stacktrace = stacktrace.value or {}
|
|
992
|
+
|
|
621
993
|
for frame in stacktrace.get("frames") or ():
|
|
622
994
|
yield frame
|
|
623
995
|
|
|
624
996
|
|
|
625
|
-
def handle_in_app(event, in_app_exclude=None, in_app_include=None):
|
|
626
|
-
# type: (
|
|
997
|
+
def handle_in_app(event, in_app_exclude=None, in_app_include=None, project_root=None):
|
|
998
|
+
# type: (Event, Optional[List[str]], Optional[List[str]], Optional[str]) -> Event
|
|
627
999
|
for stacktrace in iter_event_stacktraces(event):
|
|
628
|
-
|
|
1000
|
+
if isinstance(stacktrace, AnnotatedValue):
|
|
1001
|
+
stacktrace = stacktrace.value or {}
|
|
1002
|
+
|
|
1003
|
+
set_in_app_in_frames(
|
|
629
1004
|
stacktrace.get("frames"),
|
|
630
1005
|
in_app_exclude=in_app_exclude,
|
|
631
1006
|
in_app_include=in_app_include,
|
|
1007
|
+
project_root=project_root,
|
|
632
1008
|
)
|
|
633
1009
|
|
|
634
1010
|
return event
|
|
635
1011
|
|
|
636
1012
|
|
|
637
|
-
def
|
|
638
|
-
# type: (Any, Optional[List[str]], Optional[List[str]]) -> Optional[Any]
|
|
1013
|
+
def set_in_app_in_frames(frames, in_app_exclude, in_app_include, project_root=None):
|
|
1014
|
+
# type: (Any, Optional[List[str]], Optional[List[str]], Optional[str]) -> Optional[Any]
|
|
639
1015
|
if not frames:
|
|
640
1016
|
return None
|
|
641
1017
|
|
|
642
|
-
any_in_app = False
|
|
643
1018
|
for frame in frames:
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
any_in_app = True
|
|
1019
|
+
# if frame has already been marked as in_app, skip it
|
|
1020
|
+
current_in_app = frame.get("in_app")
|
|
1021
|
+
if current_in_app is not None:
|
|
648
1022
|
continue
|
|
649
1023
|
|
|
650
1024
|
module = frame.get("module")
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1025
|
+
|
|
1026
|
+
# check if module in frame is in the list of modules to include
|
|
1027
|
+
if _module_in_list(module, in_app_include):
|
|
654
1028
|
frame["in_app"] = True
|
|
655
|
-
|
|
656
|
-
|
|
1029
|
+
continue
|
|
1030
|
+
|
|
1031
|
+
# check if module in frame is in the list of modules to exclude
|
|
1032
|
+
if _module_in_list(module, in_app_exclude):
|
|
1033
|
+
frame["in_app"] = False
|
|
1034
|
+
continue
|
|
1035
|
+
|
|
1036
|
+
# if frame has no abs_path, skip further checks
|
|
1037
|
+
abs_path = frame.get("abs_path")
|
|
1038
|
+
if abs_path is None:
|
|
1039
|
+
continue
|
|
1040
|
+
|
|
1041
|
+
if _is_external_source(abs_path):
|
|
657
1042
|
frame["in_app"] = False
|
|
1043
|
+
continue
|
|
658
1044
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
frame["in_app"] = True
|
|
1045
|
+
if _is_in_project_root(abs_path, project_root):
|
|
1046
|
+
frame["in_app"] = True
|
|
1047
|
+
continue
|
|
663
1048
|
|
|
664
1049
|
return frames
|
|
665
1050
|
|
|
@@ -683,7 +1068,54 @@ def exc_info_from_error(error):
|
|
|
683
1068
|
else:
|
|
684
1069
|
raise ValueError("Expected Exception object to report, got %s!" % type(error))
|
|
685
1070
|
|
|
686
|
-
|
|
1071
|
+
exc_info = (exc_type, exc_value, tb)
|
|
1072
|
+
|
|
1073
|
+
if TYPE_CHECKING:
|
|
1074
|
+
# This cast is safe because exc_type and exc_value are either both
|
|
1075
|
+
# None or both not None.
|
|
1076
|
+
exc_info = cast(ExcInfo, exc_info)
|
|
1077
|
+
|
|
1078
|
+
return exc_info
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def merge_stack_frames(frames, full_stack, client_options):
|
|
1082
|
+
# type: (List[Dict[str, Any]], List[Dict[str, Any]], Optional[Dict[str, Any]]) -> List[Dict[str, Any]]
|
|
1083
|
+
"""
|
|
1084
|
+
Add the missing frames from full_stack to frames and return the merged list.
|
|
1085
|
+
"""
|
|
1086
|
+
frame_ids = {
|
|
1087
|
+
(
|
|
1088
|
+
frame["abs_path"],
|
|
1089
|
+
frame["context_line"],
|
|
1090
|
+
frame["lineno"],
|
|
1091
|
+
frame["function"],
|
|
1092
|
+
)
|
|
1093
|
+
for frame in frames
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
new_frames = [
|
|
1097
|
+
stackframe
|
|
1098
|
+
for stackframe in full_stack
|
|
1099
|
+
if (
|
|
1100
|
+
stackframe["abs_path"],
|
|
1101
|
+
stackframe["context_line"],
|
|
1102
|
+
stackframe["lineno"],
|
|
1103
|
+
stackframe["function"],
|
|
1104
|
+
)
|
|
1105
|
+
not in frame_ids
|
|
1106
|
+
]
|
|
1107
|
+
new_frames.extend(frames)
|
|
1108
|
+
|
|
1109
|
+
# Limit the number of frames
|
|
1110
|
+
max_stack_frames = (
|
|
1111
|
+
client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES)
|
|
1112
|
+
if client_options
|
|
1113
|
+
else None
|
|
1114
|
+
)
|
|
1115
|
+
if max_stack_frames is not None:
|
|
1116
|
+
new_frames = new_frames[len(new_frames) - max_stack_frames :]
|
|
1117
|
+
|
|
1118
|
+
return new_frames
|
|
687
1119
|
|
|
688
1120
|
|
|
689
1121
|
def event_from_exception(
|
|
@@ -691,15 +1123,24 @@ def event_from_exception(
|
|
|
691
1123
|
client_options=None, # type: Optional[Dict[str, Any]]
|
|
692
1124
|
mechanism=None, # type: Optional[Dict[str, Any]]
|
|
693
1125
|
):
|
|
694
|
-
# type: (...) -> Tuple[
|
|
1126
|
+
# type: (...) -> Tuple[Event, Dict[str, Any]]
|
|
695
1127
|
exc_info = exc_info_from_error(exc_info)
|
|
696
1128
|
hint = event_hint_with_exc_info(exc_info)
|
|
1129
|
+
|
|
1130
|
+
if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK):
|
|
1131
|
+
full_stack = current_stacktrace(
|
|
1132
|
+
include_local_variables=client_options["include_local_variables"],
|
|
1133
|
+
max_value_length=client_options["max_value_length"],
|
|
1134
|
+
)["frames"]
|
|
1135
|
+
else:
|
|
1136
|
+
full_stack = None
|
|
1137
|
+
|
|
697
1138
|
return (
|
|
698
1139
|
{
|
|
699
1140
|
"level": "error",
|
|
700
1141
|
"exception": {
|
|
701
1142
|
"values": exceptions_from_error_tuple(
|
|
702
|
-
exc_info, client_options, mechanism
|
|
1143
|
+
exc_info, client_options, mechanism, full_stack
|
|
703
1144
|
)
|
|
704
1145
|
},
|
|
705
1146
|
},
|
|
@@ -707,37 +1148,142 @@ def event_from_exception(
|
|
|
707
1148
|
)
|
|
708
1149
|
|
|
709
1150
|
|
|
710
|
-
def
|
|
711
|
-
# type: (str, Optional[List[str]]) -> bool
|
|
712
|
-
if
|
|
1151
|
+
def _module_in_list(name, items):
|
|
1152
|
+
# type: (Optional[str], Optional[List[str]]) -> bool
|
|
1153
|
+
if name is None:
|
|
1154
|
+
return False
|
|
1155
|
+
|
|
1156
|
+
if not items:
|
|
713
1157
|
return False
|
|
714
|
-
|
|
1158
|
+
|
|
1159
|
+
for item in items:
|
|
715
1160
|
if item == name or name.startswith(item + "."):
|
|
716
1161
|
return True
|
|
1162
|
+
|
|
717
1163
|
return False
|
|
718
1164
|
|
|
719
1165
|
|
|
1166
|
+
def _is_external_source(abs_path):
|
|
1167
|
+
# type: (Optional[str]) -> bool
|
|
1168
|
+
# check if frame is in 'site-packages' or 'dist-packages'
|
|
1169
|
+
if abs_path is None:
|
|
1170
|
+
return False
|
|
1171
|
+
|
|
1172
|
+
external_source = (
|
|
1173
|
+
re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None
|
|
1174
|
+
)
|
|
1175
|
+
return external_source
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def _is_in_project_root(abs_path, project_root):
|
|
1179
|
+
# type: (Optional[str], Optional[str]) -> bool
|
|
1180
|
+
if abs_path is None or project_root is None:
|
|
1181
|
+
return False
|
|
1182
|
+
|
|
1183
|
+
# check if path is in the project root
|
|
1184
|
+
if abs_path.startswith(project_root):
|
|
1185
|
+
return True
|
|
1186
|
+
|
|
1187
|
+
return False
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def _truncate_by_bytes(string, max_bytes):
|
|
1191
|
+
# type: (str, int) -> str
|
|
1192
|
+
"""
|
|
1193
|
+
Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes.
|
|
1194
|
+
"""
|
|
1195
|
+
truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore")
|
|
1196
|
+
|
|
1197
|
+
return truncated + "..."
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def _get_size_in_bytes(value):
|
|
1201
|
+
# type: (str) -> Optional[int]
|
|
1202
|
+
try:
|
|
1203
|
+
return len(value.encode("utf-8"))
|
|
1204
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
1205
|
+
return None
|
|
1206
|
+
|
|
1207
|
+
|
|
720
1208
|
def strip_string(value, max_length=None):
|
|
721
1209
|
# type: (str, Optional[int]) -> Union[AnnotatedValue, str]
|
|
722
|
-
# TODO: read max_length from config
|
|
723
1210
|
if not value:
|
|
724
1211
|
return value
|
|
725
1212
|
|
|
726
1213
|
if max_length is None:
|
|
727
|
-
|
|
728
|
-
max_length = MAX_STRING_LENGTH
|
|
1214
|
+
max_length = DEFAULT_MAX_VALUE_LENGTH
|
|
729
1215
|
|
|
730
|
-
|
|
1216
|
+
byte_size = _get_size_in_bytes(value)
|
|
1217
|
+
text_size = len(value)
|
|
731
1218
|
|
|
732
|
-
if
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1219
|
+
if byte_size is not None and byte_size > max_length:
|
|
1220
|
+
# truncate to max_length bytes, preserving code points
|
|
1221
|
+
truncated_value = _truncate_by_bytes(value, max_length)
|
|
1222
|
+
elif text_size is not None and text_size > max_length:
|
|
1223
|
+
# fallback to truncating by string length
|
|
1224
|
+
truncated_value = value[: max_length - 3] + "..."
|
|
1225
|
+
else:
|
|
1226
|
+
return value
|
|
1227
|
+
|
|
1228
|
+
return AnnotatedValue(
|
|
1229
|
+
value=truncated_value,
|
|
1230
|
+
metadata={
|
|
1231
|
+
"len": byte_size or text_size,
|
|
1232
|
+
"rem": [["!limit", "x", max_length - 3, max_length]],
|
|
1233
|
+
},
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def parse_version(version):
|
|
1238
|
+
# type: (str) -> Optional[Tuple[int, ...]]
|
|
1239
|
+
"""
|
|
1240
|
+
Parses a version string into a tuple of integers.
|
|
1241
|
+
This uses the parsing loging from PEP 440:
|
|
1242
|
+
https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
|
|
1243
|
+
"""
|
|
1244
|
+
VERSION_PATTERN = r""" # noqa: N806
|
|
1245
|
+
v?
|
|
1246
|
+
(?:
|
|
1247
|
+
(?:(?P<epoch>[0-9]+)!)? # epoch
|
|
1248
|
+
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
|
|
1249
|
+
(?P<pre> # pre-release
|
|
1250
|
+
[-_\.]?
|
|
1251
|
+
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
|
|
1252
|
+
[-_\.]?
|
|
1253
|
+
(?P<pre_n>[0-9]+)?
|
|
1254
|
+
)?
|
|
1255
|
+
(?P<post> # post release
|
|
1256
|
+
(?:-(?P<post_n1>[0-9]+))
|
|
1257
|
+
|
|
|
1258
|
+
(?:
|
|
1259
|
+
[-_\.]?
|
|
1260
|
+
(?P<post_l>post|rev|r)
|
|
1261
|
+
[-_\.]?
|
|
1262
|
+
(?P<post_n2>[0-9]+)?
|
|
1263
|
+
)
|
|
1264
|
+
)?
|
|
1265
|
+
(?P<dev> # dev release
|
|
1266
|
+
[-_\.]?
|
|
1267
|
+
(?P<dev_l>dev)
|
|
1268
|
+
[-_\.]?
|
|
1269
|
+
(?P<dev_n>[0-9]+)?
|
|
1270
|
+
)?
|
|
739
1271
|
)
|
|
740
|
-
|
|
1272
|
+
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
|
|
1273
|
+
"""
|
|
1274
|
+
|
|
1275
|
+
pattern = re.compile(
|
|
1276
|
+
r"^\s*" + VERSION_PATTERN + r"\s*$",
|
|
1277
|
+
re.VERBOSE | re.IGNORECASE,
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
try:
|
|
1281
|
+
release = pattern.match(version).groupdict()["release"] # type: ignore
|
|
1282
|
+
release_tuple = tuple(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
|
|
1283
|
+
except (TypeError, ValueError, AttributeError):
|
|
1284
|
+
return None
|
|
1285
|
+
|
|
1286
|
+
return release_tuple
|
|
741
1287
|
|
|
742
1288
|
|
|
743
1289
|
def _is_contextvars_broken():
|
|
@@ -746,12 +1292,26 @@ def _is_contextvars_broken():
|
|
|
746
1292
|
Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
|
|
747
1293
|
"""
|
|
748
1294
|
try:
|
|
749
|
-
|
|
1295
|
+
import gevent
|
|
1296
|
+
from gevent.monkey import is_object_patched
|
|
750
1297
|
|
|
1298
|
+
# Get the MAJOR and MINOR version numbers of Gevent
|
|
1299
|
+
version_tuple = tuple(
|
|
1300
|
+
[int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
|
|
1301
|
+
)
|
|
751
1302
|
if is_object_patched("threading", "local"):
|
|
752
|
-
# Gevent 20.
|
|
753
|
-
#
|
|
754
|
-
|
|
1303
|
+
# Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
|
|
1304
|
+
# context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
|
|
1305
|
+
# Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
|
|
1306
|
+
# Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
|
|
1307
|
+
# for contextvars, is able to patch both thread locals and contextvars, in
|
|
1308
|
+
# that case, check if contextvars are effectively patched.
|
|
1309
|
+
if (
|
|
1310
|
+
# Gevent 20.9.0+
|
|
1311
|
+
(sys.version_info >= (3, 7) and version_tuple >= (20, 9))
|
|
1312
|
+
# Gevent 20.5.0+ or Python < 3.7
|
|
1313
|
+
or (is_object_patched("contextvars", "ContextVar"))
|
|
1314
|
+
):
|
|
755
1315
|
return False
|
|
756
1316
|
|
|
757
1317
|
return True
|
|
@@ -759,9 +1319,18 @@ def _is_contextvars_broken():
|
|
|
759
1319
|
pass
|
|
760
1320
|
|
|
761
1321
|
try:
|
|
1322
|
+
import greenlet
|
|
762
1323
|
from eventlet.patcher import is_monkey_patched # type: ignore
|
|
763
1324
|
|
|
764
|
-
|
|
1325
|
+
greenlet_version = parse_version(greenlet.__version__)
|
|
1326
|
+
|
|
1327
|
+
if greenlet_version is None:
|
|
1328
|
+
logger.error(
|
|
1329
|
+
"Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
|
|
1330
|
+
)
|
|
1331
|
+
return False
|
|
1332
|
+
|
|
1333
|
+
if is_monkey_patched("thread") and greenlet_version < (0, 5):
|
|
765
1334
|
return True
|
|
766
1335
|
except ImportError:
|
|
767
1336
|
pass
|
|
@@ -771,21 +1340,33 @@ def _is_contextvars_broken():
|
|
|
771
1340
|
|
|
772
1341
|
def _make_threadlocal_contextvars(local):
|
|
773
1342
|
# type: (type) -> type
|
|
774
|
-
class ContextVar
|
|
1343
|
+
class ContextVar:
|
|
775
1344
|
# Super-limited impl of ContextVar
|
|
776
1345
|
|
|
777
|
-
def __init__(self, name):
|
|
778
|
-
# type: (str) -> None
|
|
1346
|
+
def __init__(self, name, default=None):
|
|
1347
|
+
# type: (str, Any) -> None
|
|
779
1348
|
self._name = name
|
|
1349
|
+
self._default = default
|
|
780
1350
|
self._local = local()
|
|
1351
|
+
self._original_local = local()
|
|
781
1352
|
|
|
782
|
-
def get(self, default):
|
|
1353
|
+
def get(self, default=None):
|
|
783
1354
|
# type: (Any) -> Any
|
|
784
|
-
return getattr(self._local, "value", default)
|
|
1355
|
+
return getattr(self._local, "value", default or self._default)
|
|
785
1356
|
|
|
786
1357
|
def set(self, value):
|
|
787
|
-
# type: (Any) ->
|
|
1358
|
+
# type: (Any) -> Any
|
|
1359
|
+
token = str(random.getrandbits(64))
|
|
1360
|
+
original_value = self.get()
|
|
1361
|
+
setattr(self._original_local, token, original_value)
|
|
788
1362
|
self._local.value = value
|
|
1363
|
+
return token
|
|
1364
|
+
|
|
1365
|
+
def reset(self, token):
|
|
1366
|
+
# type: (Any) -> None
|
|
1367
|
+
self._local.value = getattr(self._original_local, token)
|
|
1368
|
+
# delete the original value (this way it works in Python 3.6+)
|
|
1369
|
+
del self._original_local.__dict__[token]
|
|
789
1370
|
|
|
790
1371
|
return ContextVar
|
|
791
1372
|
|
|
@@ -807,7 +1388,7 @@ def _get_contextvars():
|
|
|
807
1388
|
# `aiocontextvars` is absolutely required for functional
|
|
808
1389
|
# contextvars on Python 3.6.
|
|
809
1390
|
try:
|
|
810
|
-
from aiocontextvars import ContextVar
|
|
1391
|
+
from aiocontextvars import ContextVar
|
|
811
1392
|
|
|
812
1393
|
return True, ContextVar
|
|
813
1394
|
except ImportError:
|
|
@@ -840,9 +1421,12 @@ Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more in
|
|
|
840
1421
|
"""
|
|
841
1422
|
|
|
842
1423
|
|
|
843
|
-
def
|
|
1424
|
+
def qualname_from_function(func):
|
|
844
1425
|
# type: (Callable[..., Any]) -> Optional[str]
|
|
845
|
-
|
|
1426
|
+
"""Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
|
|
1427
|
+
func_qualname = None # type: Optional[str]
|
|
1428
|
+
|
|
1429
|
+
# Python 2
|
|
846
1430
|
try:
|
|
847
1431
|
return "%s.%s.%s" % (
|
|
848
1432
|
func.im_class.__module__, # type: ignore
|
|
@@ -852,30 +1436,44 @@ def transaction_from_function(func):
|
|
|
852
1436
|
except Exception:
|
|
853
1437
|
pass
|
|
854
1438
|
|
|
855
|
-
|
|
856
|
-
getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None
|
|
857
|
-
) # type: Optional[str]
|
|
1439
|
+
prefix, suffix = "", ""
|
|
858
1440
|
|
|
859
|
-
if
|
|
860
|
-
|
|
861
|
-
|
|
1441
|
+
if isinstance(func, partial) and hasattr(func.func, "__name__"):
|
|
1442
|
+
prefix, suffix = "partial(<function ", ">)"
|
|
1443
|
+
func = func.func
|
|
1444
|
+
else:
|
|
1445
|
+
# The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
|
|
1446
|
+
# https://github.com/python/cpython/pull/16600
|
|
1447
|
+
partial_method = getattr(func, "_partialmethod", None) or getattr(
|
|
1448
|
+
func, "__partialmethod__", None
|
|
1449
|
+
)
|
|
1450
|
+
if isinstance(partial_method, partialmethod):
|
|
1451
|
+
prefix, suffix = "partialmethod(<function ", ">)"
|
|
1452
|
+
func = partial_method.func
|
|
862
1453
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
#
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1454
|
+
if hasattr(func, "__qualname__"):
|
|
1455
|
+
func_qualname = func.__qualname__
|
|
1456
|
+
elif hasattr(func, "__name__"): # Python 2.7 has no __qualname__
|
|
1457
|
+
func_qualname = func.__name__
|
|
1458
|
+
|
|
1459
|
+
# Python 3: methods, functions, classes
|
|
1460
|
+
if func_qualname is not None:
|
|
1461
|
+
if hasattr(func, "__module__") and isinstance(func.__module__, str):
|
|
1462
|
+
func_qualname = func.__module__ + "." + func_qualname
|
|
1463
|
+
func_qualname = prefix + func_qualname + suffix
|
|
870
1464
|
|
|
871
|
-
# Possibly a lambda
|
|
872
1465
|
return func_qualname
|
|
873
1466
|
|
|
874
1467
|
|
|
1468
|
+
def transaction_from_function(func):
|
|
1469
|
+
# type: (Callable[..., Any]) -> Optional[str]
|
|
1470
|
+
return qualname_from_function(func)
|
|
1471
|
+
|
|
1472
|
+
|
|
875
1473
|
disable_capture_event = ContextVar("disable_capture_event")
|
|
876
1474
|
|
|
877
1475
|
|
|
878
|
-
class ServerlessTimeoutWarning(Exception):
|
|
1476
|
+
class ServerlessTimeoutWarning(Exception): # noqa: N818
|
|
879
1477
|
"""Raised when a serverless method is about to reach its timeout."""
|
|
880
1478
|
|
|
881
1479
|
pass
|
|
@@ -886,16 +1484,44 @@ class TimeoutThread(threading.Thread):
|
|
|
886
1484
|
waiting_time and raises a custom ServerlessTimeout exception.
|
|
887
1485
|
"""
|
|
888
1486
|
|
|
889
|
-
def __init__(
|
|
890
|
-
|
|
1487
|
+
def __init__(
|
|
1488
|
+
self, waiting_time, configured_timeout, isolation_scope=None, current_scope=None
|
|
1489
|
+
):
|
|
1490
|
+
# type: (float, int, Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]) -> None
|
|
891
1491
|
threading.Thread.__init__(self)
|
|
892
1492
|
self.waiting_time = waiting_time
|
|
893
1493
|
self.configured_timeout = configured_timeout
|
|
894
1494
|
|
|
1495
|
+
self.isolation_scope = isolation_scope
|
|
1496
|
+
self.current_scope = current_scope
|
|
1497
|
+
|
|
1498
|
+
self._stop_event = threading.Event()
|
|
1499
|
+
|
|
1500
|
+
def stop(self):
|
|
1501
|
+
# type: () -> None
|
|
1502
|
+
self._stop_event.set()
|
|
1503
|
+
|
|
1504
|
+
def _capture_exception(self):
|
|
1505
|
+
# type: () -> ExcInfo
|
|
1506
|
+
exc_info = sys.exc_info()
|
|
1507
|
+
|
|
1508
|
+
client = sentry_sdk.get_client()
|
|
1509
|
+
event, hint = event_from_exception(
|
|
1510
|
+
exc_info,
|
|
1511
|
+
client_options=client.options,
|
|
1512
|
+
mechanism={"type": "threading", "handled": False},
|
|
1513
|
+
)
|
|
1514
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
1515
|
+
|
|
1516
|
+
return exc_info
|
|
1517
|
+
|
|
895
1518
|
def run(self):
|
|
896
1519
|
# type: () -> None
|
|
897
1520
|
|
|
898
|
-
|
|
1521
|
+
self._stop_event.wait(self.waiting_time)
|
|
1522
|
+
|
|
1523
|
+
if self._stop_event.is_set():
|
|
1524
|
+
return
|
|
899
1525
|
|
|
900
1526
|
integer_configured_timeout = int(self.configured_timeout)
|
|
901
1527
|
|
|
@@ -904,8 +1530,549 @@ class TimeoutThread(threading.Thread):
|
|
|
904
1530
|
integer_configured_timeout = integer_configured_timeout + 1
|
|
905
1531
|
|
|
906
1532
|
# Raising Exception after timeout duration is reached
|
|
1533
|
+
if self.isolation_scope is not None and self.current_scope is not None:
|
|
1534
|
+
with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
|
|
1535
|
+
with sentry_sdk.scope.use_scope(self.current_scope):
|
|
1536
|
+
try:
|
|
1537
|
+
raise ServerlessTimeoutWarning(
|
|
1538
|
+
"WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
|
|
1539
|
+
integer_configured_timeout
|
|
1540
|
+
)
|
|
1541
|
+
)
|
|
1542
|
+
except Exception:
|
|
1543
|
+
reraise(*self._capture_exception())
|
|
1544
|
+
|
|
907
1545
|
raise ServerlessTimeoutWarning(
|
|
908
1546
|
"WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
|
|
909
1547
|
integer_configured_timeout
|
|
910
1548
|
)
|
|
911
1549
|
)
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def to_base64(original):
|
|
1553
|
+
# type: (str) -> Optional[str]
|
|
1554
|
+
"""
|
|
1555
|
+
Convert a string to base64, via UTF-8. Returns None on invalid input.
|
|
1556
|
+
"""
|
|
1557
|
+
base64_string = None
|
|
1558
|
+
|
|
1559
|
+
try:
|
|
1560
|
+
utf8_bytes = original.encode("UTF-8")
|
|
1561
|
+
base64_bytes = base64.b64encode(utf8_bytes)
|
|
1562
|
+
base64_string = base64_bytes.decode("UTF-8")
|
|
1563
|
+
except Exception as err:
|
|
1564
|
+
logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
|
|
1565
|
+
|
|
1566
|
+
return base64_string
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def from_base64(base64_string):
|
|
1570
|
+
# type: (str) -> Optional[str]
|
|
1571
|
+
"""
|
|
1572
|
+
Convert a string from base64, via UTF-8. Returns None on invalid input.
|
|
1573
|
+
"""
|
|
1574
|
+
utf8_string = None
|
|
1575
|
+
|
|
1576
|
+
try:
|
|
1577
|
+
only_valid_chars = BASE64_ALPHABET.match(base64_string)
|
|
1578
|
+
assert only_valid_chars
|
|
1579
|
+
|
|
1580
|
+
base64_bytes = base64_string.encode("UTF-8")
|
|
1581
|
+
utf8_bytes = base64.b64decode(base64_bytes)
|
|
1582
|
+
utf8_string = utf8_bytes.decode("UTF-8")
|
|
1583
|
+
except Exception as err:
|
|
1584
|
+
logger.warning(
|
|
1585
|
+
"Unable to decode {b64} from base64:".format(b64=base64_string), err
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
return utf8_string
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
def sanitize_url(url, remove_authority=True, remove_query_values=True, split=False):
|
|
1595
|
+
# type: (str, bool, bool, bool) -> Union[str, Components]
|
|
1596
|
+
"""
|
|
1597
|
+
Removes the authority and query parameter values from a given URL.
|
|
1598
|
+
"""
|
|
1599
|
+
parsed_url = urlsplit(url)
|
|
1600
|
+
query_params = parse_qs(parsed_url.query, keep_blank_values=True)
|
|
1601
|
+
|
|
1602
|
+
# strip username:password (netloc can be usr:pwd@example.com)
|
|
1603
|
+
if remove_authority:
|
|
1604
|
+
netloc_parts = parsed_url.netloc.split("@")
|
|
1605
|
+
if len(netloc_parts) > 1:
|
|
1606
|
+
netloc = "%s:%s@%s" % (
|
|
1607
|
+
SENSITIVE_DATA_SUBSTITUTE,
|
|
1608
|
+
SENSITIVE_DATA_SUBSTITUTE,
|
|
1609
|
+
netloc_parts[-1],
|
|
1610
|
+
)
|
|
1611
|
+
else:
|
|
1612
|
+
netloc = parsed_url.netloc
|
|
1613
|
+
else:
|
|
1614
|
+
netloc = parsed_url.netloc
|
|
1615
|
+
|
|
1616
|
+
# strip values from query string
|
|
1617
|
+
if remove_query_values:
|
|
1618
|
+
query_string = unquote(
|
|
1619
|
+
urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
|
|
1620
|
+
)
|
|
1621
|
+
else:
|
|
1622
|
+
query_string = parsed_url.query
|
|
1623
|
+
|
|
1624
|
+
components = Components(
|
|
1625
|
+
scheme=parsed_url.scheme,
|
|
1626
|
+
netloc=netloc,
|
|
1627
|
+
query=query_string,
|
|
1628
|
+
path=parsed_url.path,
|
|
1629
|
+
fragment=parsed_url.fragment,
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
if split:
|
|
1633
|
+
return components
|
|
1634
|
+
else:
|
|
1635
|
+
return urlunsplit(components)
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
def parse_url(url, sanitize=True):
|
|
1642
|
+
# type: (str, bool) -> ParsedUrl
|
|
1643
|
+
"""
|
|
1644
|
+
Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
|
|
1645
|
+
parameters will be sanitized to remove sensitive data. The autority (username and password)
|
|
1646
|
+
in the URL will always be removed.
|
|
1647
|
+
"""
|
|
1648
|
+
parsed_url = sanitize_url(
|
|
1649
|
+
url, remove_authority=True, remove_query_values=sanitize, split=True
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
base_url = urlunsplit(
|
|
1653
|
+
Components(
|
|
1654
|
+
scheme=parsed_url.scheme, # type: ignore
|
|
1655
|
+
netloc=parsed_url.netloc, # type: ignore
|
|
1656
|
+
query="",
|
|
1657
|
+
path=parsed_url.path, # type: ignore
|
|
1658
|
+
fragment="",
|
|
1659
|
+
)
|
|
1660
|
+
)
|
|
1661
|
+
|
|
1662
|
+
return ParsedUrl(
|
|
1663
|
+
url=base_url,
|
|
1664
|
+
query=parsed_url.query, # type: ignore
|
|
1665
|
+
fragment=parsed_url.fragment, # type: ignore
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
def is_valid_sample_rate(rate, source):
|
|
1670
|
+
# type: (Any, str) -> bool
|
|
1671
|
+
"""
|
|
1672
|
+
Checks the given sample rate to make sure it is valid type and value (a
|
|
1673
|
+
boolean or a number between 0 and 1, inclusive).
|
|
1674
|
+
"""
|
|
1675
|
+
|
|
1676
|
+
# both booleans and NaN are instances of Real, so a) checking for Real
|
|
1677
|
+
# checks for the possibility of a boolean also, and b) we have to check
|
|
1678
|
+
# separately for NaN and Decimal does not derive from Real so need to check that too
|
|
1679
|
+
if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
|
|
1680
|
+
logger.warning(
|
|
1681
|
+
"{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
|
|
1682
|
+
source=source, rate=rate, type=type(rate)
|
|
1683
|
+
)
|
|
1684
|
+
)
|
|
1685
|
+
return False
|
|
1686
|
+
|
|
1687
|
+
# in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
|
|
1688
|
+
rate = float(rate)
|
|
1689
|
+
if rate < 0 or rate > 1:
|
|
1690
|
+
logger.warning(
|
|
1691
|
+
"{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
|
|
1692
|
+
source=source, rate=rate
|
|
1693
|
+
)
|
|
1694
|
+
)
|
|
1695
|
+
return False
|
|
1696
|
+
|
|
1697
|
+
return True
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
def match_regex_list(item, regex_list=None, substring_matching=False):
|
|
1701
|
+
# type: (str, Optional[List[str]], bool) -> bool
|
|
1702
|
+
if regex_list is None:
|
|
1703
|
+
return False
|
|
1704
|
+
|
|
1705
|
+
for item_matcher in regex_list:
|
|
1706
|
+
if not substring_matching and item_matcher[-1] != "$":
|
|
1707
|
+
item_matcher += "$"
|
|
1708
|
+
|
|
1709
|
+
matched = re.search(item_matcher, item)
|
|
1710
|
+
if matched:
|
|
1711
|
+
return True
|
|
1712
|
+
|
|
1713
|
+
return False
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
def is_sentry_url(client, url):
|
|
1717
|
+
# type: (sentry_sdk.client.BaseClient, str) -> bool
|
|
1718
|
+
"""
|
|
1719
|
+
Determines whether the given URL matches the Sentry DSN.
|
|
1720
|
+
"""
|
|
1721
|
+
return (
|
|
1722
|
+
client is not None
|
|
1723
|
+
and client.transport is not None
|
|
1724
|
+
and client.transport.parsed_dsn is not None
|
|
1725
|
+
and client.transport.parsed_dsn.netloc in url
|
|
1726
|
+
)
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def _generate_installed_modules():
|
|
1730
|
+
# type: () -> Iterator[Tuple[str, str]]
|
|
1731
|
+
try:
|
|
1732
|
+
from importlib import metadata
|
|
1733
|
+
|
|
1734
|
+
yielded = set()
|
|
1735
|
+
for dist in metadata.distributions():
|
|
1736
|
+
name = dist.metadata.get("Name", None) # type: ignore[attr-defined]
|
|
1737
|
+
# `metadata` values may be `None`, see:
|
|
1738
|
+
# https://github.com/python/cpython/issues/91216
|
|
1739
|
+
# and
|
|
1740
|
+
# https://github.com/python/importlib_metadata/issues/371
|
|
1741
|
+
if name is not None:
|
|
1742
|
+
normalized_name = _normalize_module_name(name)
|
|
1743
|
+
if dist.version is not None and normalized_name not in yielded:
|
|
1744
|
+
yield normalized_name, dist.version
|
|
1745
|
+
yielded.add(normalized_name)
|
|
1746
|
+
|
|
1747
|
+
except ImportError:
|
|
1748
|
+
# < py3.8
|
|
1749
|
+
try:
|
|
1750
|
+
import pkg_resources
|
|
1751
|
+
except ImportError:
|
|
1752
|
+
return
|
|
1753
|
+
|
|
1754
|
+
for info in pkg_resources.working_set:
|
|
1755
|
+
yield _normalize_module_name(info.key), info.version
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def _normalize_module_name(name):
|
|
1759
|
+
# type: (str) -> str
|
|
1760
|
+
return name.lower()
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
def _replace_hyphens_dots_and_underscores_with_dashes(name):
|
|
1764
|
+
# type: (str) -> str
|
|
1765
|
+
# https://peps.python.org/pep-0503/#normalized-names
|
|
1766
|
+
return re.sub(r"[-_.]+", "-", name)
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
def _get_installed_modules():
|
|
1770
|
+
# type: () -> Dict[str, str]
|
|
1771
|
+
global _installed_modules
|
|
1772
|
+
if _installed_modules is None:
|
|
1773
|
+
_installed_modules = dict(_generate_installed_modules())
|
|
1774
|
+
return _installed_modules
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
def package_version(package):
|
|
1778
|
+
# type: (str) -> Optional[Tuple[int, ...]]
|
|
1779
|
+
normalized_package = _normalize_module_name(
|
|
1780
|
+
_replace_hyphens_dots_and_underscores_with_dashes(package)
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
installed_packages = {
|
|
1784
|
+
_replace_hyphens_dots_and_underscores_with_dashes(module): v
|
|
1785
|
+
for module, v in _get_installed_modules().items()
|
|
1786
|
+
}
|
|
1787
|
+
version = installed_packages.get(normalized_package)
|
|
1788
|
+
if version is None:
|
|
1789
|
+
return None
|
|
1790
|
+
|
|
1791
|
+
return parse_version(version)
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def reraise(tp, value, tb=None):
|
|
1795
|
+
# type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> NoReturn
|
|
1796
|
+
assert value is not None
|
|
1797
|
+
if value.__traceback__ is not tb:
|
|
1798
|
+
raise value.with_traceback(tb)
|
|
1799
|
+
raise value
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def _no_op(*_a, **_k):
|
|
1803
|
+
# type: (*Any, **Any) -> None
|
|
1804
|
+
"""No-op function for ensure_integration_enabled."""
|
|
1805
|
+
pass
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
if TYPE_CHECKING:
|
|
1809
|
+
|
|
1810
|
+
@overload
|
|
1811
|
+
def ensure_integration_enabled(
|
|
1812
|
+
integration, # type: type[sentry_sdk.integrations.Integration]
|
|
1813
|
+
original_function, # type: Callable[P, R]
|
|
1814
|
+
):
|
|
1815
|
+
# type: (...) -> Callable[[Callable[P, R]], Callable[P, R]]
|
|
1816
|
+
...
|
|
1817
|
+
|
|
1818
|
+
@overload
|
|
1819
|
+
def ensure_integration_enabled(
|
|
1820
|
+
integration, # type: type[sentry_sdk.integrations.Integration]
|
|
1821
|
+
):
|
|
1822
|
+
# type: (...) -> Callable[[Callable[P, None]], Callable[P, None]]
|
|
1823
|
+
...
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def ensure_integration_enabled(
|
|
1827
|
+
integration, # type: type[sentry_sdk.integrations.Integration]
|
|
1828
|
+
original_function=_no_op, # type: Union[Callable[P, R], Callable[P, None]]
|
|
1829
|
+
):
|
|
1830
|
+
# type: (...) -> Callable[[Callable[P, R]], Callable[P, R]]
|
|
1831
|
+
"""
|
|
1832
|
+
Ensures a given integration is enabled prior to calling a Sentry-patched function.
|
|
1833
|
+
|
|
1834
|
+
The function takes as its parameters the integration that must be enabled and the original
|
|
1835
|
+
function that the SDK is patching. The function returns a function that takes the
|
|
1836
|
+
decorated (Sentry-patched) function as its parameter, and returns a function that, when
|
|
1837
|
+
called, checks whether the given integration is enabled. If the integration is enabled, the
|
|
1838
|
+
function calls the decorated, Sentry-patched function. If the integration is not enabled,
|
|
1839
|
+
the original function is called.
|
|
1840
|
+
|
|
1841
|
+
The function also takes care of preserving the original function's signature and docstring.
|
|
1842
|
+
|
|
1843
|
+
Example usage:
|
|
1844
|
+
|
|
1845
|
+
```python
|
|
1846
|
+
@ensure_integration_enabled(MyIntegration, my_function)
|
|
1847
|
+
def patch_my_function():
|
|
1848
|
+
with sentry_sdk.start_transaction(...):
|
|
1849
|
+
return my_function()
|
|
1850
|
+
```
|
|
1851
|
+
"""
|
|
1852
|
+
if TYPE_CHECKING:
|
|
1853
|
+
# Type hint to ensure the default function has the right typing. The overloads
|
|
1854
|
+
# ensure the default _no_op function is only used when R is None.
|
|
1855
|
+
original_function = cast(Callable[P, R], original_function)
|
|
1856
|
+
|
|
1857
|
+
def patcher(sentry_patched_function):
|
|
1858
|
+
# type: (Callable[P, R]) -> Callable[P, R]
|
|
1859
|
+
def runner(*args: "P.args", **kwargs: "P.kwargs"):
|
|
1860
|
+
# type: (...) -> R
|
|
1861
|
+
if sentry_sdk.get_client().get_integration(integration) is None:
|
|
1862
|
+
return original_function(*args, **kwargs)
|
|
1863
|
+
|
|
1864
|
+
return sentry_patched_function(*args, **kwargs)
|
|
1865
|
+
|
|
1866
|
+
if original_function is _no_op:
|
|
1867
|
+
return wraps(sentry_patched_function)(runner)
|
|
1868
|
+
|
|
1869
|
+
return wraps(original_function)(runner)
|
|
1870
|
+
|
|
1871
|
+
return patcher
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
if PY37:
|
|
1875
|
+
|
|
1876
|
+
def nanosecond_time():
|
|
1877
|
+
# type: () -> int
|
|
1878
|
+
return time.perf_counter_ns()
|
|
1879
|
+
|
|
1880
|
+
else:
|
|
1881
|
+
|
|
1882
|
+
def nanosecond_time():
|
|
1883
|
+
# type: () -> int
|
|
1884
|
+
return int(time.perf_counter() * 1e9)
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
def now():
|
|
1888
|
+
# type: () -> float
|
|
1889
|
+
return time.perf_counter()
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
try:
|
|
1893
|
+
from gevent import get_hub as get_gevent_hub
|
|
1894
|
+
from gevent.monkey import is_module_patched
|
|
1895
|
+
except ImportError:
|
|
1896
|
+
# it's not great that the signatures are different, get_hub can't return None
|
|
1897
|
+
# consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
|
|
1898
|
+
def get_gevent_hub(): # type: ignore[misc]
|
|
1899
|
+
# type: () -> Optional[Hub]
|
|
1900
|
+
return None
|
|
1901
|
+
|
|
1902
|
+
def is_module_patched(mod_name):
|
|
1903
|
+
# type: (str) -> bool
|
|
1904
|
+
# unable to import from gevent means no modules have been patched
|
|
1905
|
+
return False
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
def is_gevent():
|
|
1909
|
+
# type: () -> bool
|
|
1910
|
+
return is_module_patched("threading") or is_module_patched("_thread")
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
def get_current_thread_meta(thread=None):
|
|
1914
|
+
# type: (Optional[threading.Thread]) -> Tuple[Optional[int], Optional[str]]
|
|
1915
|
+
"""
|
|
1916
|
+
Try to get the id of the current thread, with various fall backs.
|
|
1917
|
+
"""
|
|
1918
|
+
|
|
1919
|
+
# if a thread is specified, that takes priority
|
|
1920
|
+
if thread is not None:
|
|
1921
|
+
try:
|
|
1922
|
+
thread_id = thread.ident
|
|
1923
|
+
thread_name = thread.name
|
|
1924
|
+
if thread_id is not None:
|
|
1925
|
+
return thread_id, thread_name
|
|
1926
|
+
except AttributeError:
|
|
1927
|
+
pass
|
|
1928
|
+
|
|
1929
|
+
# if the app is using gevent, we should look at the gevent hub first
|
|
1930
|
+
# as the id there differs from what the threading module reports
|
|
1931
|
+
if is_gevent():
|
|
1932
|
+
gevent_hub = get_gevent_hub()
|
|
1933
|
+
if gevent_hub is not None:
|
|
1934
|
+
try:
|
|
1935
|
+
# this is undocumented, so wrap it in try except to be safe
|
|
1936
|
+
return gevent_hub.thread_ident, None
|
|
1937
|
+
except AttributeError:
|
|
1938
|
+
pass
|
|
1939
|
+
|
|
1940
|
+
# use the current thread's id if possible
|
|
1941
|
+
try:
|
|
1942
|
+
thread = threading.current_thread()
|
|
1943
|
+
thread_id = thread.ident
|
|
1944
|
+
thread_name = thread.name
|
|
1945
|
+
if thread_id is not None:
|
|
1946
|
+
return thread_id, thread_name
|
|
1947
|
+
except AttributeError:
|
|
1948
|
+
pass
|
|
1949
|
+
|
|
1950
|
+
# if we can't get the current thread id, fall back to the main thread id
|
|
1951
|
+
try:
|
|
1952
|
+
thread = threading.main_thread()
|
|
1953
|
+
thread_id = thread.ident
|
|
1954
|
+
thread_name = thread.name
|
|
1955
|
+
if thread_id is not None:
|
|
1956
|
+
return thread_id, thread_name
|
|
1957
|
+
except AttributeError:
|
|
1958
|
+
pass
|
|
1959
|
+
|
|
1960
|
+
# we've tried everything, time to give up
|
|
1961
|
+
return None, None
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
def should_be_treated_as_error(ty, value):
|
|
1965
|
+
# type: (Any, Any) -> bool
|
|
1966
|
+
if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
|
|
1967
|
+
# https://docs.python.org/3/library/exceptions.html#SystemExit
|
|
1968
|
+
return False
|
|
1969
|
+
|
|
1970
|
+
return True
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
if TYPE_CHECKING:
|
|
1974
|
+
T = TypeVar("T")
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
def try_convert(convert_func, value):
|
|
1978
|
+
# type: (Callable[[Any], T], Any) -> Optional[T]
|
|
1979
|
+
"""
|
|
1980
|
+
Attempt to convert from an unknown type to a specific type, using the
|
|
1981
|
+
given function. Return None if the conversion fails, i.e. if the function
|
|
1982
|
+
raises an exception.
|
|
1983
|
+
"""
|
|
1984
|
+
try:
|
|
1985
|
+
if isinstance(value, convert_func): # type: ignore
|
|
1986
|
+
return value
|
|
1987
|
+
except TypeError:
|
|
1988
|
+
pass
|
|
1989
|
+
|
|
1990
|
+
try:
|
|
1991
|
+
return convert_func(value)
|
|
1992
|
+
except Exception:
|
|
1993
|
+
return None
|
|
1994
|
+
|
|
1995
|
+
|
|
1996
|
+
def safe_serialize(data):
|
|
1997
|
+
# type: (Any) -> str
|
|
1998
|
+
"""Safely serialize to a readable string."""
|
|
1999
|
+
|
|
2000
|
+
def serialize_item(item):
|
|
2001
|
+
# type: (Any) -> Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]
|
|
2002
|
+
if callable(item):
|
|
2003
|
+
try:
|
|
2004
|
+
module = getattr(item, "__module__", None)
|
|
2005
|
+
qualname = getattr(item, "__qualname__", None)
|
|
2006
|
+
name = getattr(item, "__name__", "anonymous")
|
|
2007
|
+
|
|
2008
|
+
if module and qualname:
|
|
2009
|
+
full_path = f"{module}.{qualname}"
|
|
2010
|
+
elif module and name:
|
|
2011
|
+
full_path = f"{module}.{name}"
|
|
2012
|
+
else:
|
|
2013
|
+
full_path = name
|
|
2014
|
+
|
|
2015
|
+
return f"<function {full_path}>"
|
|
2016
|
+
except Exception:
|
|
2017
|
+
return f"<callable {type(item).__name__}>"
|
|
2018
|
+
elif isinstance(item, dict):
|
|
2019
|
+
return {k: serialize_item(v) for k, v in item.items()}
|
|
2020
|
+
elif isinstance(item, (list, tuple)):
|
|
2021
|
+
return [serialize_item(x) for x in item]
|
|
2022
|
+
elif hasattr(item, "__dict__"):
|
|
2023
|
+
try:
|
|
2024
|
+
attrs = {
|
|
2025
|
+
k: serialize_item(v)
|
|
2026
|
+
for k, v in vars(item).items()
|
|
2027
|
+
if not k.startswith("_")
|
|
2028
|
+
}
|
|
2029
|
+
return f"<{type(item).__name__} {attrs}>"
|
|
2030
|
+
except Exception:
|
|
2031
|
+
return repr(item)
|
|
2032
|
+
else:
|
|
2033
|
+
return item
|
|
2034
|
+
|
|
2035
|
+
try:
|
|
2036
|
+
serialized = serialize_item(data)
|
|
2037
|
+
return json.dumps(serialized, default=str)
|
|
2038
|
+
except Exception:
|
|
2039
|
+
return str(data)
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def has_logs_enabled(options):
|
|
2043
|
+
# type: (Optional[dict[str, Any]]) -> bool
|
|
2044
|
+
if options is None:
|
|
2045
|
+
return False
|
|
2046
|
+
|
|
2047
|
+
return bool(
|
|
2048
|
+
options.get("enable_logs", False)
|
|
2049
|
+
or options["_experiments"].get("enable_logs", False)
|
|
2050
|
+
)
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
def get_before_send_log(options):
|
|
2054
|
+
# type: (Optional[dict[str, Any]]) -> Optional[Callable[[Log, Hint], Optional[Log]]]
|
|
2055
|
+
if options is None:
|
|
2056
|
+
return None
|
|
2057
|
+
|
|
2058
|
+
return options.get("before_send_log") or options["_experiments"].get(
|
|
2059
|
+
"before_send_log"
|
|
2060
|
+
)
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
def has_metrics_enabled(options):
|
|
2064
|
+
# type: (Optional[dict[str, Any]]) -> bool
|
|
2065
|
+
if options is None:
|
|
2066
|
+
return False
|
|
2067
|
+
|
|
2068
|
+
return bool(options.get("enable_metrics", True))
|
|
2069
|
+
|
|
2070
|
+
|
|
2071
|
+
def get_before_send_metric(options):
|
|
2072
|
+
# type: (Optional[dict[str, Any]]) -> Optional[Callable[[Metric, Hint], Optional[Metric]]]
|
|
2073
|
+
if options is None:
|
|
2074
|
+
return None
|
|
2075
|
+
|
|
2076
|
+
return options.get("before_send_metric") or options["_experiments"].get(
|
|
2077
|
+
"before_send_metric"
|
|
2078
|
+
)
|