sentry-sdk 0.7.5__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sentry_sdk/__init__.py +48 -30
- sentry_sdk/_compat.py +74 -61
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +289 -0
- sentry_sdk/_types.py +338 -0
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +496 -80
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +1023 -103
- sentry_sdk/consts.py +1438 -66
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +15 -14
- sentry_sdk/envelope.py +369 -0
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +611 -280
- sentry_sdk/integrations/__init__.py +276 -49
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +180 -44
- sentry_sdk/integrations/aiohttp.py +291 -42
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +9 -8
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +341 -0
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +17 -10
- sentry_sdk/integrations/aws_lambda.py +377 -62
- sentry_sdk/integrations/beam.py +176 -0
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +221 -0
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +134 -0
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +48 -14
- sentry_sdk/integrations/django/__init__.py +584 -191
- sentry_sdk/integrations/django/asgi.py +245 -0
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +187 -0
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +79 -5
- sentry_sdk/integrations/django/transactions.py +49 -22
- sentry_sdk/integrations/django/views.py +96 -0
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +50 -13
- sentry_sdk/integrations/executing.py +67 -0
- sentry_sdk/integrations/falcon.py +272 -0
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +142 -88
- sentry_sdk/integrations/gcp.py +239 -0
- sentry_sdk/integrations/gnu_backtrace.py +99 -0
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +307 -96
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +14 -31
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +141 -0
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +112 -68
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +95 -37
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +294 -123
- sentry_sdk/integrations/serverless.py +48 -19
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/__init__.py +4 -0
- sentry_sdk/integrations/spark/spark_driver.py +316 -0
- sentry_sdk/integrations/spark/spark_worker.py +116 -0
- sentry_sdk/integrations/sqlalchemy.py +142 -0
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +235 -29
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +158 -28
- sentry_sdk/integrations/tornado.py +84 -52
- sentry_sdk/integrations/trytond.py +50 -0
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +201 -119
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/py.typed +0 -0
- sentry_sdk/scope.py +1713 -85
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +405 -0
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +275 -0
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1486 -0
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +806 -134
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1625 -465
- sentry_sdk/worker.py +54 -25
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/integrations/celery.py +0 -119
- sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
- sentry_sdk-0.7.5.dist-info/METADATA +0 -36
- sentry_sdk-0.7.5.dist-info/RECORD +0 -39
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/utils.py
CHANGED
|
@@ -1,85 +1,282 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
3
|
import linecache
|
|
4
4
|
import logging
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import random
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
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
|
|
5
19
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
26
|
+
|
|
27
|
+
import sentry_sdk
|
|
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,
|
|
17
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
|
+
)
|
|
18
59
|
|
|
19
|
-
|
|
20
|
-
from typing import Any
|
|
21
|
-
from typing import Dict
|
|
22
|
-
from typing import Union
|
|
23
|
-
from typing import Iterator
|
|
24
|
-
from typing import Tuple
|
|
25
|
-
from typing import Optional
|
|
26
|
-
from typing import List
|
|
27
|
-
from typing import Set
|
|
28
|
-
from typing import Type
|
|
29
|
-
|
|
30
|
-
from sentry_sdk.consts import ClientOptions
|
|
31
|
-
|
|
32
|
-
ExcInfo = Tuple[
|
|
33
|
-
Optional[Type[BaseException]], Optional[BaseException], Optional[Any]
|
|
34
|
-
]
|
|
60
|
+
from gevent.hub import Hub
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
# Importing ABCs from collections is deprecated, and will stop working in 3.8
|
|
38
|
-
# https://github.com/python/cpython/blob/master/Lib/collections/__init__.py#L49
|
|
39
|
-
from collections import Mapping, Sequence
|
|
40
|
-
else:
|
|
41
|
-
# New in 3.3
|
|
42
|
-
# https://docs.python.org/3/library/collections.abc.html
|
|
43
|
-
from collections.abc import Mapping, Sequence
|
|
62
|
+
from sentry_sdk._types import Event, ExcInfo, Log, Hint, Metric
|
|
44
63
|
|
|
45
|
-
|
|
64
|
+
P = ParamSpec("P")
|
|
65
|
+
R = TypeVar("R")
|
|
46
66
|
|
|
47
67
|
|
|
68
|
+
epoch = datetime(1970, 1, 1)
|
|
69
|
+
|
|
48
70
|
# The logger is created here but initialized in the debug support module
|
|
49
71
|
logger = logging.getLogger("sentry_sdk.errors")
|
|
50
72
|
|
|
51
|
-
|
|
73
|
+
_installed_modules = None
|
|
52
74
|
|
|
75
|
+
BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$")
|
|
53
76
|
|
|
54
|
-
|
|
77
|
+
FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0"))
|
|
78
|
+
TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1"))
|
|
55
79
|
|
|
80
|
+
MAX_STACK_FRAMES = 2000
|
|
81
|
+
"""Maximum number of stack frames to send to Sentry.
|
|
56
82
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
"""
|
|
59
88
|
|
|
60
89
|
|
|
61
|
-
def
|
|
62
|
-
#
|
|
63
|
-
|
|
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
|
|
64
96
|
|
|
97
|
+
if normalized in FALSY_ENV_VALUES:
|
|
98
|
+
return False
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
if normalized in TRUTHY_ENV_VALUES:
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
return None if strict else bool(value)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def json_dumps(data):
|
|
107
|
+
# type: (Any) -> bytes
|
|
108
|
+
"""Serialize data into a compact JSON representation encoded as UTF-8."""
|
|
109
|
+
return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_git_revision():
|
|
113
|
+
# type: () -> Optional[str]
|
|
69
114
|
try:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
|
162
|
+
|
|
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:
|
|
201
|
+
__slots__ = ()
|
|
202
|
+
|
|
203
|
+
def __enter__(self):
|
|
204
|
+
# type: () -> ContextManager[Any]
|
|
205
|
+
return self
|
|
206
|
+
|
|
207
|
+
def __exit__(self, ty, value, tb):
|
|
208
|
+
# type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> bool
|
|
209
|
+
if ty is not None and value is not None:
|
|
210
|
+
capture_internal_exception((ty, value, tb))
|
|
211
|
+
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def capture_internal_exceptions():
|
|
219
|
+
# type: () -> ContextManager[Any]
|
|
220
|
+
return _CAPTURE_INTERNAL_EXCEPTION
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def capture_internal_exception(exc_info):
|
|
224
|
+
# type: (ExcInfo) -> None
|
|
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)
|
|
75
233
|
|
|
76
234
|
|
|
77
235
|
def to_timestamp(value):
|
|
236
|
+
# type: (datetime) -> float
|
|
78
237
|
return (value - epoch).total_seconds()
|
|
79
238
|
|
|
80
239
|
|
|
240
|
+
def format_timestamp(value):
|
|
241
|
+
# type: (datetime) -> str
|
|
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)
|
|
276
|
+
|
|
277
|
+
|
|
81
278
|
def event_hint_with_exc_info(exc_info=None):
|
|
82
|
-
# type: (ExcInfo) -> Dict[str, Optional[ExcInfo]]
|
|
279
|
+
# type: (Optional[ExcInfo]) -> Dict[str, Optional[ExcInfo]]
|
|
83
280
|
"""Creates a hint with the exc info filled in."""
|
|
84
281
|
if exc_info is None:
|
|
85
282
|
exc_info = sys.exc_info()
|
|
@@ -94,31 +291,40 @@ class BadDsn(ValueError):
|
|
|
94
291
|
"""Raised on invalid DSNs."""
|
|
95
292
|
|
|
96
293
|
|
|
97
|
-
|
|
98
|
-
class Dsn(object):
|
|
294
|
+
class Dsn:
|
|
99
295
|
"""Represents a DSN."""
|
|
100
296
|
|
|
101
297
|
def __init__(self, value):
|
|
298
|
+
# type: (Union[Dsn, str]) -> None
|
|
102
299
|
if isinstance(value, Dsn):
|
|
103
300
|
self.__dict__ = dict(value.__dict__)
|
|
104
301
|
return
|
|
105
|
-
parts =
|
|
106
|
-
|
|
302
|
+
parts = urlsplit(str(value))
|
|
303
|
+
|
|
304
|
+
if parts.scheme not in ("http", "https"):
|
|
107
305
|
raise BadDsn("Unsupported scheme %r" % parts.scheme)
|
|
108
306
|
self.scheme = parts.scheme
|
|
307
|
+
|
|
308
|
+
if parts.hostname is None:
|
|
309
|
+
raise BadDsn("Missing hostname")
|
|
310
|
+
|
|
109
311
|
self.host = parts.hostname
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
self.port = self.scheme == "https" and 443 or 80
|
|
113
|
-
|
|
114
|
-
|
|
312
|
+
|
|
313
|
+
if parts.port is None:
|
|
314
|
+
self.port = self.scheme == "https" and 443 or 80 # type: int
|
|
315
|
+
else:
|
|
316
|
+
self.port = parts.port
|
|
317
|
+
|
|
318
|
+
if not parts.username:
|
|
115
319
|
raise BadDsn("Missing public key")
|
|
320
|
+
|
|
321
|
+
self.public_key = parts.username
|
|
116
322
|
self.secret_key = parts.password
|
|
117
323
|
|
|
118
324
|
path = parts.path.rsplit("/", 1)
|
|
119
325
|
|
|
120
326
|
try:
|
|
121
|
-
self.project_id =
|
|
327
|
+
self.project_id = str(int(path.pop()))
|
|
122
328
|
except (ValueError, TypeError):
|
|
123
329
|
raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:])
|
|
124
330
|
|
|
@@ -126,6 +332,7 @@ class Dsn(object):
|
|
|
126
332
|
|
|
127
333
|
@property
|
|
128
334
|
def netloc(self):
|
|
335
|
+
# type: () -> str
|
|
129
336
|
"""The netloc part of a DSN."""
|
|
130
337
|
rv = self.host
|
|
131
338
|
if (self.scheme, self.port) not in (("http", 80), ("https", 443)):
|
|
@@ -133,6 +340,7 @@ class Dsn(object):
|
|
|
133
340
|
return rv
|
|
134
341
|
|
|
135
342
|
def to_auth(self, client=None):
|
|
343
|
+
# type: (Optional[Any]) -> Auth
|
|
136
344
|
"""Returns the auth info object for this dsn."""
|
|
137
345
|
return Auth(
|
|
138
346
|
scheme=self.scheme,
|
|
@@ -145,6 +353,7 @@ class Dsn(object):
|
|
|
145
353
|
)
|
|
146
354
|
|
|
147
355
|
def __str__(self):
|
|
356
|
+
# type: () -> str
|
|
148
357
|
return "%s://%s%s@%s%s%s" % (
|
|
149
358
|
self.scheme,
|
|
150
359
|
self.public_key,
|
|
@@ -155,7 +364,7 @@ class Dsn(object):
|
|
|
155
364
|
)
|
|
156
365
|
|
|
157
366
|
|
|
158
|
-
class Auth
|
|
367
|
+
class Auth:
|
|
159
368
|
"""Helper object that represents the auth info."""
|
|
160
369
|
|
|
161
370
|
def __init__(
|
|
@@ -169,6 +378,7 @@ class Auth(object):
|
|
|
169
378
|
client=None,
|
|
170
379
|
path="/",
|
|
171
380
|
):
|
|
381
|
+
# type: (str, str, str, str, Optional[str], int, Optional[Any], str) -> None
|
|
172
382
|
self.scheme = scheme
|
|
173
383
|
self.host = host
|
|
174
384
|
self.path = path
|
|
@@ -178,35 +388,38 @@ class Auth(object):
|
|
|
178
388
|
self.version = version
|
|
179
389
|
self.client = client
|
|
180
390
|
|
|
181
|
-
|
|
182
|
-
|
|
391
|
+
def get_api_url(
|
|
392
|
+
self,
|
|
393
|
+
type=EndpointType.ENVELOPE, # type: EndpointType
|
|
394
|
+
):
|
|
395
|
+
# type: (...) -> str
|
|
183
396
|
"""Returns the API url for storing events."""
|
|
184
|
-
return "%s://%s%sapi/%s/
|
|
397
|
+
return "%s://%s%sapi/%s/%s/" % (
|
|
185
398
|
self.scheme,
|
|
186
399
|
self.host,
|
|
187
400
|
self.path,
|
|
188
401
|
self.project_id,
|
|
402
|
+
type.value,
|
|
189
403
|
)
|
|
190
404
|
|
|
191
|
-
def to_header(self
|
|
405
|
+
def to_header(self):
|
|
406
|
+
# type: () -> str
|
|
192
407
|
"""Returns the auth header a string."""
|
|
193
408
|
rv = [("sentry_key", self.public_key), ("sentry_version", self.version)]
|
|
194
|
-
if timestamp is not None:
|
|
195
|
-
rv.append(("sentry_timestamp", str(to_timestamp(timestamp))))
|
|
196
409
|
if self.client is not None:
|
|
197
410
|
rv.append(("sentry_client", self.client))
|
|
198
411
|
if self.secret_key is not None:
|
|
199
412
|
rv.append(("sentry_secret", self.secret_key))
|
|
200
|
-
return
|
|
413
|
+
return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv)
|
|
201
414
|
|
|
202
415
|
|
|
203
416
|
def get_type_name(cls):
|
|
204
|
-
# type: (
|
|
417
|
+
# type: (Optional[type]) -> Optional[str]
|
|
205
418
|
return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None)
|
|
206
419
|
|
|
207
420
|
|
|
208
421
|
def get_type_module(cls):
|
|
209
|
-
# type: (
|
|
422
|
+
# type: (Optional[type]) -> Optional[str]
|
|
210
423
|
mod = getattr(cls, "__module__", None)
|
|
211
424
|
if mod not in (None, "builtins", "__builtins__"):
|
|
212
425
|
return mod
|
|
@@ -214,10 +427,11 @@ def get_type_module(cls):
|
|
|
214
427
|
|
|
215
428
|
|
|
216
429
|
def should_hide_frame(frame):
|
|
217
|
-
# type: (
|
|
430
|
+
# type: (FrameType) -> bool
|
|
218
431
|
try:
|
|
219
432
|
mod = frame.f_globals["__name__"]
|
|
220
|
-
|
|
433
|
+
if mod.startswith("sentry_sdk."):
|
|
434
|
+
return True
|
|
221
435
|
except (AttributeError, KeyError):
|
|
222
436
|
pass
|
|
223
437
|
|
|
@@ -232,34 +446,27 @@ def should_hide_frame(frame):
|
|
|
232
446
|
|
|
233
447
|
|
|
234
448
|
def iter_stacks(tb):
|
|
235
|
-
# type: (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def slim_string(value, length=512):
|
|
243
|
-
# type: (str, int) -> str
|
|
244
|
-
if not value:
|
|
245
|
-
return value
|
|
246
|
-
if len(value) > length:
|
|
247
|
-
return value[: length - 3] + "..."
|
|
248
|
-
return value[:length]
|
|
449
|
+
# type: (Optional[TracebackType]) -> Iterator[TracebackType]
|
|
450
|
+
tb_ = tb # type: Optional[TracebackType]
|
|
451
|
+
while tb_ is not None:
|
|
452
|
+
if not should_hide_frame(tb_.tb_frame):
|
|
453
|
+
yield tb_
|
|
454
|
+
tb_ = tb_.tb_next
|
|
249
455
|
|
|
250
456
|
|
|
251
457
|
def get_lines_from_file(
|
|
252
458
|
filename, # type: str
|
|
253
459
|
lineno, # type: int
|
|
254
|
-
|
|
255
|
-
|
|
460
|
+
max_length=None, # type: Optional[int]
|
|
461
|
+
loader=None, # type: Optional[Any]
|
|
462
|
+
module=None, # type: Optional[str]
|
|
256
463
|
):
|
|
257
|
-
# type: (...) -> Tuple[List[str], Optional[str], List[str]]
|
|
464
|
+
# type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
|
|
258
465
|
context_lines = 5
|
|
259
466
|
source = None
|
|
260
467
|
if loader is not None and hasattr(loader, "get_source"):
|
|
261
468
|
try:
|
|
262
|
-
source_str = loader.get_source(module)
|
|
469
|
+
source_str = loader.get_source(module) # type: Optional[str]
|
|
263
470
|
except (ImportError, IOError):
|
|
264
471
|
source_str = None
|
|
265
472
|
if source_str is not None:
|
|
@@ -279,11 +486,12 @@ def get_lines_from_file(
|
|
|
279
486
|
|
|
280
487
|
try:
|
|
281
488
|
pre_context = [
|
|
282
|
-
|
|
489
|
+
strip_string(line.strip("\r\n"), max_length=max_length)
|
|
490
|
+
for line in source[lower_bound:lineno]
|
|
283
491
|
]
|
|
284
|
-
context_line =
|
|
492
|
+
context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length)
|
|
285
493
|
post_context = [
|
|
286
|
-
|
|
494
|
+
strip_string(line.strip("\r\n"), max_length=max_length)
|
|
287
495
|
for line in source[(lineno + 1) : upper_bound]
|
|
288
496
|
]
|
|
289
497
|
return pre_context, context_line, post_context
|
|
@@ -292,10 +500,14 @@ def get_lines_from_file(
|
|
|
292
500
|
return [], None, []
|
|
293
501
|
|
|
294
502
|
|
|
295
|
-
def get_source_context(
|
|
296
|
-
# type:
|
|
503
|
+
def get_source_context(
|
|
504
|
+
frame, # type: FrameType
|
|
505
|
+
tb_lineno, # type: Optional[int]
|
|
506
|
+
max_value_length=None, # type: Optional[int]
|
|
507
|
+
):
|
|
508
|
+
# type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
|
|
297
509
|
try:
|
|
298
|
-
abs_path = frame.f_code.co_filename
|
|
510
|
+
abs_path = frame.f_code.co_filename # type: Optional[str]
|
|
299
511
|
except Exception:
|
|
300
512
|
abs_path = None
|
|
301
513
|
try:
|
|
@@ -306,16 +518,20 @@ def get_source_context(frame, tb_lineno):
|
|
|
306
518
|
loader = frame.f_globals["__loader__"]
|
|
307
519
|
except Exception:
|
|
308
520
|
loader = None
|
|
309
|
-
|
|
310
|
-
if
|
|
311
|
-
|
|
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
|
+
|
|
312
528
|
return [], None, []
|
|
313
529
|
|
|
314
530
|
|
|
315
531
|
def safe_str(value):
|
|
316
532
|
# type: (Any) -> str
|
|
317
533
|
try:
|
|
318
|
-
return
|
|
534
|
+
return str(value)
|
|
319
535
|
except Exception:
|
|
320
536
|
return safe_repr(value)
|
|
321
537
|
|
|
@@ -323,77 +539,16 @@ def safe_str(value):
|
|
|
323
539
|
def safe_repr(value):
|
|
324
540
|
# type: (Any) -> str
|
|
325
541
|
try:
|
|
326
|
-
|
|
327
|
-
if isinstance(rv, bytes):
|
|
328
|
-
rv = rv.decode("utf-8", "replace")
|
|
329
|
-
|
|
330
|
-
# At this point `rv` contains a bunch of literal escape codes, like
|
|
331
|
-
# this (exaggerated example):
|
|
332
|
-
#
|
|
333
|
-
# u"\\x2f"
|
|
334
|
-
#
|
|
335
|
-
# But we want to show this string as:
|
|
336
|
-
#
|
|
337
|
-
# u"/"
|
|
338
|
-
try:
|
|
339
|
-
# unicode-escape does this job, but can only decode latin1. So we
|
|
340
|
-
# attempt to encode in latin1.
|
|
341
|
-
return rv.encode("latin1").decode("unicode-escape")
|
|
342
|
-
except Exception:
|
|
343
|
-
# Since usually strings aren't latin1 this can break. In those
|
|
344
|
-
# cases we just give up.
|
|
345
|
-
return rv
|
|
542
|
+
return repr(value)
|
|
346
543
|
except Exception:
|
|
347
|
-
|
|
348
|
-
return u"<broken repr>"
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def object_to_json(obj, remaining_depth=4, memo=None):
|
|
352
|
-
if memo is None:
|
|
353
|
-
memo = Memo()
|
|
354
|
-
if memo.memoize(obj):
|
|
355
|
-
return CYCLE_MARKER
|
|
356
|
-
|
|
357
|
-
try:
|
|
358
|
-
if remaining_depth > 0:
|
|
359
|
-
hints = {"memo": memo, "remaining_depth": remaining_depth}
|
|
360
|
-
for processor in global_repr_processors:
|
|
361
|
-
with capture_internal_exceptions():
|
|
362
|
-
result = processor(obj, hints)
|
|
363
|
-
if result is not NotImplemented:
|
|
364
|
-
return result
|
|
365
|
-
|
|
366
|
-
if isinstance(obj, (list, tuple)):
|
|
367
|
-
# It is not safe to iterate over another sequence types as this may raise errors or
|
|
368
|
-
# bring undesired side-effects (e.g. Django querysets are executed during iteration)
|
|
369
|
-
return [
|
|
370
|
-
object_to_json(x, remaining_depth=remaining_depth - 1, memo=memo)
|
|
371
|
-
for x in obj
|
|
372
|
-
]
|
|
373
|
-
|
|
374
|
-
if isinstance(obj, Mapping):
|
|
375
|
-
return {
|
|
376
|
-
safe_str(k): object_to_json(
|
|
377
|
-
v, remaining_depth=remaining_depth - 1, memo=memo
|
|
378
|
-
)
|
|
379
|
-
for k, v in obj.items()
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return safe_repr(obj)
|
|
383
|
-
finally:
|
|
384
|
-
memo.unmemoize(obj)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def extract_locals(frame):
|
|
388
|
-
# type: (Any) -> Dict[str, Any]
|
|
389
|
-
rv = {}
|
|
390
|
-
for key, value in frame.f_locals.items():
|
|
391
|
-
rv[str(key)] = object_to_json(value)
|
|
392
|
-
return rv
|
|
544
|
+
return "<broken repr>"
|
|
393
545
|
|
|
394
546
|
|
|
395
547
|
def filename_for_module(module, abs_path):
|
|
396
|
-
# type: (str, str) -> str
|
|
548
|
+
# type: (Optional[str], Optional[str]) -> Optional[str]
|
|
549
|
+
if not abs_path or not module:
|
|
550
|
+
return abs_path
|
|
551
|
+
|
|
397
552
|
try:
|
|
398
553
|
if abs_path.endswith(".pyc"):
|
|
399
554
|
abs_path = abs_path[:-1]
|
|
@@ -403,6 +558,9 @@ def filename_for_module(module, abs_path):
|
|
|
403
558
|
return os.path.basename(abs_path)
|
|
404
559
|
|
|
405
560
|
base_module_path = sys.modules[base_module].__file__
|
|
561
|
+
if not base_module_path:
|
|
562
|
+
return abs_path
|
|
563
|
+
|
|
406
564
|
return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip(
|
|
407
565
|
os.sep
|
|
408
566
|
)
|
|
@@ -410,15 +568,22 @@ def filename_for_module(module, abs_path):
|
|
|
410
568
|
return abs_path
|
|
411
569
|
|
|
412
570
|
|
|
413
|
-
def serialize_frame(
|
|
414
|
-
|
|
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]
|
|
415
580
|
f_code = getattr(frame, "f_code", None)
|
|
416
|
-
if f_code:
|
|
417
|
-
abs_path = frame.f_code.co_filename
|
|
418
|
-
function = frame.f_code.co_name
|
|
419
|
-
else:
|
|
581
|
+
if not f_code:
|
|
420
582
|
abs_path = None
|
|
421
583
|
function = None
|
|
584
|
+
else:
|
|
585
|
+
abs_path = frame.f_code.co_filename
|
|
586
|
+
function = frame.f_code.co_name
|
|
422
587
|
try:
|
|
423
588
|
module = frame.f_globals["__name__"]
|
|
424
589
|
except Exception:
|
|
@@ -427,43 +592,54 @@ def serialize_frame(frame, tb_lineno=None, with_locals=True):
|
|
|
427
592
|
if tb_lineno is None:
|
|
428
593
|
tb_lineno = frame.f_lineno
|
|
429
594
|
|
|
430
|
-
|
|
595
|
+
try:
|
|
596
|
+
os_abs_path = os.path.abspath(abs_path) if abs_path else None
|
|
597
|
+
except Exception:
|
|
598
|
+
os_abs_path = None
|
|
431
599
|
|
|
432
600
|
rv = {
|
|
433
601
|
"filename": filename_for_module(module, abs_path) or None,
|
|
434
|
-
"abs_path":
|
|
602
|
+
"abs_path": os_abs_path,
|
|
435
603
|
"function": function or "<unknown>",
|
|
436
604
|
"module": module,
|
|
437
605
|
"lineno": tb_lineno,
|
|
438
|
-
|
|
439
|
-
"context_line": context_line,
|
|
440
|
-
"post_context": post_context,
|
|
441
|
-
}
|
|
442
|
-
if with_locals:
|
|
443
|
-
rv["vars"] = extract_locals(frame)
|
|
444
|
-
return rv
|
|
606
|
+
} # type: Dict[str, Any]
|
|
445
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
|
+
)
|
|
446
612
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
"
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
]
|
|
456
|
-
}
|
|
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
|
+
)
|
|
619
|
+
|
|
620
|
+
return rv
|
|
457
621
|
|
|
458
622
|
|
|
459
|
-
def current_stacktrace(
|
|
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]
|
|
460
629
|
__tracebackhide__ = True
|
|
461
630
|
frames = []
|
|
462
631
|
|
|
463
|
-
f = sys._getframe()
|
|
632
|
+
f = sys._getframe() # type: Optional[FrameType]
|
|
464
633
|
while f is not None:
|
|
465
634
|
if not should_hide_frame(f):
|
|
466
|
-
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
|
+
)
|
|
467
643
|
f = f.f_back
|
|
468
644
|
|
|
469
645
|
frames.reverse()
|
|
@@ -476,36 +652,126 @@ def get_errno(exc_value):
|
|
|
476
652
|
return getattr(exc_value, "errno", None)
|
|
477
653
|
|
|
478
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
|
+
|
|
479
674
|
def single_exception_from_error_tuple(
|
|
480
675
|
exc_type, # type: Optional[type]
|
|
481
676
|
exc_value, # type: Optional[BaseException]
|
|
482
|
-
tb, # type: Optional[
|
|
483
|
-
client_options=None, # type: Optional[
|
|
484
|
-
mechanism=None, # type: Dict[str, Any]
|
|
677
|
+
tb, # type: Optional[TracebackType]
|
|
678
|
+
client_options=None, # type: Optional[Dict[str, Any]]
|
|
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]]]
|
|
485
684
|
):
|
|
486
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
|
+
|
|
487
699
|
if exc_value is not None:
|
|
488
700
|
errno = get_errno(exc_value)
|
|
489
701
|
else:
|
|
490
702
|
errno = None
|
|
491
703
|
|
|
492
704
|
if errno is not None:
|
|
493
|
-
mechanism
|
|
494
|
-
|
|
495
|
-
|
|
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)
|
|
496
729
|
|
|
497
730
|
if client_options is None:
|
|
498
|
-
|
|
731
|
+
include_local_variables = True
|
|
732
|
+
include_source_context = True
|
|
733
|
+
max_value_length = DEFAULT_MAX_VALUE_LENGTH # fallback
|
|
734
|
+
custom_repr = None
|
|
499
735
|
else:
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
"
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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")
|
|
740
|
+
|
|
741
|
+
frames = [
|
|
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
|
+
)
|
|
765
|
+
|
|
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)
|
|
771
|
+
|
|
772
|
+
exception_value["stacktrace"] = {"frames": new_frames}
|
|
773
|
+
|
|
774
|
+
return exception_value
|
|
509
775
|
|
|
510
776
|
|
|
511
777
|
HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__")
|
|
@@ -532,7 +798,7 @@ if HAS_CHAINED_EXCEPTIONS:
|
|
|
532
798
|
seen_exceptions.append(exc_value)
|
|
533
799
|
seen_exception_ids.add(id(exc_value))
|
|
534
800
|
|
|
535
|
-
if exc_value.__suppress_context__:
|
|
801
|
+
if exc_value.__suppress_context__:
|
|
536
802
|
cause = exc_value.__cause__
|
|
537
803
|
else:
|
|
538
804
|
cause = exc_value.__context__
|
|
@@ -542,7 +808,6 @@ if HAS_CHAINED_EXCEPTIONS:
|
|
|
542
808
|
exc_value = cause
|
|
543
809
|
tb = getattr(cause, "__traceback__", None)
|
|
544
810
|
|
|
545
|
-
|
|
546
811
|
else:
|
|
547
812
|
|
|
548
813
|
def walk_exception_chain(exc_info):
|
|
@@ -550,70 +815,238 @@ else:
|
|
|
550
815
|
yield exc_info
|
|
551
816
|
|
|
552
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
|
+
|
|
553
921
|
def exceptions_from_error_tuple(
|
|
554
922
|
exc_info, # type: ExcInfo
|
|
555
|
-
client_options=None, # type: Optional[
|
|
556
|
-
mechanism=None, # type: Dict[str, Any]
|
|
923
|
+
client_options=None, # type: Optional[Dict[str, Any]]
|
|
924
|
+
mechanism=None, # type: Optional[Dict[str, Any]]
|
|
925
|
+
full_stack=None, # type: Optional[list[dict[str, Any]]]
|
|
557
926
|
):
|
|
558
927
|
# type: (...) -> List[Dict[str, Any]]
|
|
559
928
|
exc_type, exc_value, tb = exc_info
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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,
|
|
566
944
|
)
|
|
567
|
-
|
|
945
|
+
|
|
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
|
+
)
|
|
959
|
+
|
|
960
|
+
exceptions.reverse()
|
|
961
|
+
|
|
962
|
+
return exceptions
|
|
568
963
|
|
|
569
964
|
|
|
570
965
|
def to_string(value):
|
|
571
966
|
# type: (str) -> str
|
|
572
967
|
try:
|
|
573
|
-
return
|
|
968
|
+
return str(value)
|
|
574
969
|
except UnicodeDecodeError:
|
|
575
970
|
return repr(value)[1:-1]
|
|
576
971
|
|
|
577
972
|
|
|
578
|
-
def
|
|
579
|
-
# type: (
|
|
580
|
-
stacktraces = []
|
|
973
|
+
def iter_event_stacktraces(event):
|
|
974
|
+
# type: (Event) -> Iterator[Annotated[Dict[str, Any]]]
|
|
581
975
|
if "stacktrace" in event:
|
|
582
|
-
|
|
976
|
+
yield event["stacktrace"]
|
|
977
|
+
if "threads" in event:
|
|
978
|
+
for thread in event["threads"].get("values") or ():
|
|
979
|
+
if "stacktrace" in thread:
|
|
980
|
+
yield thread["stacktrace"]
|
|
583
981
|
if "exception" in event:
|
|
584
982
|
for exception in event["exception"].get("values") or ():
|
|
585
|
-
if "stacktrace" in exception:
|
|
586
|
-
|
|
587
|
-
|
|
983
|
+
if isinstance(exception, dict) and "stacktrace" in exception:
|
|
984
|
+
yield exception["stacktrace"]
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def iter_event_frames(event):
|
|
988
|
+
# type: (Event) -> Iterator[Dict[str, Any]]
|
|
989
|
+
for stacktrace in iter_event_stacktraces(event):
|
|
990
|
+
if isinstance(stacktrace, AnnotatedValue):
|
|
991
|
+
stacktrace = stacktrace.value or {}
|
|
992
|
+
|
|
588
993
|
for frame in stacktrace.get("frames") or ():
|
|
589
994
|
yield frame
|
|
590
995
|
|
|
591
996
|
|
|
592
|
-
def handle_in_app(event, in_app_exclude=None, in_app_include=None):
|
|
593
|
-
# type: (
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
999
|
+
for stacktrace in iter_event_stacktraces(event):
|
|
1000
|
+
if isinstance(stacktrace, AnnotatedValue):
|
|
1001
|
+
stacktrace = stacktrace.value or {}
|
|
1002
|
+
|
|
1003
|
+
set_in_app_in_frames(
|
|
1004
|
+
stacktrace.get("frames"),
|
|
1005
|
+
in_app_exclude=in_app_exclude,
|
|
1006
|
+
in_app_include=in_app_include,
|
|
1007
|
+
project_root=project_root,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
return event
|
|
1011
|
+
|
|
1012
|
+
|
|
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]
|
|
1015
|
+
if not frames:
|
|
1016
|
+
return None
|
|
1017
|
+
|
|
1018
|
+
for frame in frames:
|
|
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:
|
|
600
1022
|
continue
|
|
601
1023
|
|
|
602
1024
|
module = frame.get("module")
|
|
603
|
-
|
|
1025
|
+
|
|
1026
|
+
# check if module in frame is in the list of modules to include
|
|
1027
|
+
if _module_in_list(module, in_app_include):
|
|
1028
|
+
frame["in_app"] = True
|
|
604
1029
|
continue
|
|
605
1030
|
|
|
606
|
-
if
|
|
1031
|
+
# check if module in frame is in the list of modules to exclude
|
|
1032
|
+
if _module_in_list(module, in_app_exclude):
|
|
607
1033
|
frame["in_app"] = False
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
|
611
1040
|
|
|
612
|
-
|
|
613
|
-
|
|
1041
|
+
if _is_external_source(abs_path):
|
|
1042
|
+
frame["in_app"] = False
|
|
1043
|
+
continue
|
|
1044
|
+
|
|
1045
|
+
if _is_in_project_root(abs_path, project_root):
|
|
614
1046
|
frame["in_app"] = True
|
|
1047
|
+
continue
|
|
615
1048
|
|
|
616
|
-
return
|
|
1049
|
+
return frames
|
|
617
1050
|
|
|
618
1051
|
|
|
619
1052
|
def exc_info_from_error(error):
|
|
@@ -633,25 +1066,81 @@ def exc_info_from_error(error):
|
|
|
633
1066
|
exc_type = type(error)
|
|
634
1067
|
|
|
635
1068
|
else:
|
|
636
|
-
raise ValueError()
|
|
1069
|
+
raise ValueError("Expected Exception object to report, got %s!" % type(error))
|
|
1070
|
+
|
|
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 :]
|
|
637
1117
|
|
|
638
|
-
return
|
|
1118
|
+
return new_frames
|
|
639
1119
|
|
|
640
1120
|
|
|
641
1121
|
def event_from_exception(
|
|
642
1122
|
exc_info, # type: Union[BaseException, ExcInfo]
|
|
643
|
-
client_options=None, # type: Optional[
|
|
644
|
-
mechanism=None, # type: Dict[str, Any]
|
|
1123
|
+
client_options=None, # type: Optional[Dict[str, Any]]
|
|
1124
|
+
mechanism=None, # type: Optional[Dict[str, Any]]
|
|
645
1125
|
):
|
|
646
|
-
# type: (...) -> Tuple[
|
|
1126
|
+
# type: (...) -> Tuple[Event, Dict[str, Any]]
|
|
647
1127
|
exc_info = exc_info_from_error(exc_info)
|
|
648
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
|
+
|
|
649
1138
|
return (
|
|
650
1139
|
{
|
|
651
1140
|
"level": "error",
|
|
652
1141
|
"exception": {
|
|
653
1142
|
"values": exceptions_from_error_tuple(
|
|
654
|
-
exc_info, client_options, mechanism
|
|
1143
|
+
exc_info, client_options, mechanism, full_stack
|
|
655
1144
|
)
|
|
656
1145
|
},
|
|
657
1146
|
},
|
|
@@ -659,260 +1148,931 @@ def event_from_exception(
|
|
|
659
1148
|
)
|
|
660
1149
|
|
|
661
1150
|
|
|
662
|
-
def
|
|
663
|
-
# type: (str, Optional[List]) -> bool
|
|
664
|
-
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:
|
|
665
1157
|
return False
|
|
666
|
-
|
|
1158
|
+
|
|
1159
|
+
for item in items:
|
|
667
1160
|
if item == name or name.startswith(item + "."):
|
|
668
1161
|
return True
|
|
1162
|
+
|
|
669
1163
|
return False
|
|
670
1164
|
|
|
671
1165
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
self.metadata = metadata
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
def flatten_metadata(obj):
|
|
680
|
-
# type: (Dict[str, Any]) -> Dict[str, Any]
|
|
681
|
-
def inner(obj):
|
|
682
|
-
# type: (Any) -> Any
|
|
683
|
-
if isinstance(obj, Mapping):
|
|
684
|
-
dict_rv = {}
|
|
685
|
-
meta = {}
|
|
686
|
-
for k, v in obj.items():
|
|
687
|
-
# if we actually have "" keys in our data, throw them away. It's
|
|
688
|
-
# unclear how we would tell them apart from metadata
|
|
689
|
-
if k == "":
|
|
690
|
-
continue
|
|
691
|
-
|
|
692
|
-
dict_rv[k], meta[k] = inner(v)
|
|
693
|
-
if meta[k] is None:
|
|
694
|
-
del meta[k]
|
|
695
|
-
if dict_rv[k] is None:
|
|
696
|
-
del dict_rv[k]
|
|
697
|
-
return dict_rv, (meta or None)
|
|
698
|
-
if isinstance(obj, Sequence) and not isinstance(obj, (text_type, bytes)):
|
|
699
|
-
list_rv = []
|
|
700
|
-
meta = {}
|
|
701
|
-
for i, v in enumerate(obj):
|
|
702
|
-
new_v, meta[str(i)] = inner(v)
|
|
703
|
-
list_rv.append(new_v)
|
|
704
|
-
if meta[str(i)] is None:
|
|
705
|
-
del meta[str(i)]
|
|
706
|
-
return list_rv, (meta or None)
|
|
707
|
-
if isinstance(obj, AnnotatedValue):
|
|
708
|
-
return obj.value, {"": obj.metadata}
|
|
709
|
-
return obj, None
|
|
710
|
-
|
|
711
|
-
obj, meta = inner(obj)
|
|
712
|
-
if meta is not None:
|
|
713
|
-
obj["_meta"] = meta
|
|
714
|
-
return obj
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
def strip_event_mut(event):
|
|
718
|
-
# type: (Dict[str, Any]) -> None
|
|
719
|
-
strip_stacktrace_mut(event.get("stacktrace", None))
|
|
720
|
-
exception = event.get("exception", None)
|
|
721
|
-
if exception:
|
|
722
|
-
for exception in exception.get("values", None) or ():
|
|
723
|
-
strip_stacktrace_mut(exception.get("stacktrace", None))
|
|
724
|
-
|
|
725
|
-
strip_request_mut(event.get("request", None))
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
def strip_stacktrace_mut(stacktrace):
|
|
729
|
-
# type: (Optional[Dict[str, List[Dict[str, Any]]]]) -> None
|
|
730
|
-
if not stacktrace:
|
|
731
|
-
return
|
|
732
|
-
for frame in stacktrace.get("frames", None) or ():
|
|
733
|
-
strip_frame_mut(frame)
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
def strip_request_mut(request):
|
|
737
|
-
# type: (Dict[str, Any]) -> None
|
|
738
|
-
if not request:
|
|
739
|
-
return
|
|
740
|
-
data = request.get("data", None)
|
|
741
|
-
if not data:
|
|
742
|
-
return
|
|
743
|
-
request["data"] = strip_databag(data)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
def strip_frame_mut(frame):
|
|
747
|
-
# type: (Dict[str, Any]) -> None
|
|
748
|
-
if "vars" in frame:
|
|
749
|
-
frame["vars"] = strip_databag(frame["vars"])
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
class Memo(object):
|
|
753
|
-
def __init__(self):
|
|
754
|
-
self._inner = {}
|
|
755
|
-
|
|
756
|
-
def memoize(self, obj):
|
|
757
|
-
if id(obj) in self._inner:
|
|
758
|
-
return True
|
|
759
|
-
self._inner[id(obj)] = obj
|
|
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:
|
|
760
1170
|
return False
|
|
761
1171
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if
|
|
771
|
-
return
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if
|
|
775
|
-
return
|
|
776
|
-
if not isinstance(obj, string_types + number_types):
|
|
777
|
-
return safe_repr(obj)
|
|
778
|
-
if isinstance(obj, bytes):
|
|
779
|
-
return obj.decode("utf-8", "replace")
|
|
780
|
-
return obj
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
def strip_databag(obj, remaining_depth=20):
|
|
784
|
-
# type: (Any, int) -> Any
|
|
785
|
-
assert not isinstance(obj, bytes), "bytes should have been normalized before"
|
|
786
|
-
if remaining_depth <= 0:
|
|
787
|
-
return AnnotatedValue(None, {"rem": [["!limit", "x"]]})
|
|
788
|
-
if isinstance(obj, text_type):
|
|
789
|
-
return strip_string(obj)
|
|
790
|
-
if isinstance(obj, Mapping):
|
|
791
|
-
return {k: strip_databag(v, remaining_depth - 1) for k, v in obj.items()}
|
|
792
|
-
if isinstance(obj, Sequence):
|
|
793
|
-
return [strip_databag(v, remaining_depth - 1) for v in obj]
|
|
794
|
-
return obj
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
def strip_string(value, max_length=512):
|
|
798
|
-
# type: (str, int) -> Union[AnnotatedValue, str]
|
|
799
|
-
# TODO: read max_length from config
|
|
800
|
-
if not value:
|
|
801
|
-
return value
|
|
802
|
-
length = len(value)
|
|
803
|
-
if length > max_length:
|
|
804
|
-
return AnnotatedValue(
|
|
805
|
-
value=value[: max_length - 3] + u"...",
|
|
806
|
-
metadata={
|
|
807
|
-
"len": length,
|
|
808
|
-
"rem": [["!limit", "x", max_length - 3, max_length]],
|
|
809
|
-
},
|
|
810
|
-
)
|
|
811
|
-
return value
|
|
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
|
|
812
1186
|
|
|
1187
|
+
return False
|
|
813
1188
|
|
|
814
|
-
def format_and_strip(template, params, strip_string=strip_string):
|
|
815
|
-
"""Format a string containing %s for placeholders and call `strip_string`
|
|
816
|
-
on each parameter. The string template itself does not have a maximum
|
|
817
|
-
length.
|
|
818
1189
|
|
|
819
|
-
|
|
1190
|
+
def _truncate_by_bytes(string, max_bytes):
|
|
1191
|
+
# type: (str, int) -> str
|
|
820
1192
|
"""
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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")
|
|
824
1196
|
|
|
825
|
-
|
|
826
|
-
rv_remarks = []
|
|
827
|
-
rv_original_length = 0
|
|
828
|
-
rv_length = 0
|
|
829
|
-
rv = []
|
|
1197
|
+
return truncated + "..."
|
|
830
1198
|
|
|
831
|
-
def realign_remark(remark):
|
|
832
|
-
return [
|
|
833
|
-
(rv_length + x if isinstance(x, int_types) and i < 4 else x)
|
|
834
|
-
for i, x in enumerate(remark)
|
|
835
|
-
]
|
|
836
1199
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
stripped_param = strip_string(param)
|
|
846
|
-
if isinstance(stripped_param, AnnotatedValue):
|
|
847
|
-
rv_remarks.extend(
|
|
848
|
-
realign_remark(remark) for remark in stripped_param.metadata["rem"]
|
|
849
|
-
)
|
|
850
|
-
stripped_param = stripped_param.value
|
|
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
|
+
|
|
851
1207
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1208
|
+
def strip_string(value, max_length=None):
|
|
1209
|
+
# type: (str, Optional[int]) -> Union[AnnotatedValue, str]
|
|
1210
|
+
if not value:
|
|
1211
|
+
return value
|
|
855
1212
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
rv_original_length += len(chunks[-1])
|
|
1213
|
+
if max_length is None:
|
|
1214
|
+
max_length = DEFAULT_MAX_VALUE_LENGTH
|
|
859
1215
|
|
|
860
|
-
|
|
861
|
-
|
|
1216
|
+
byte_size = _get_size_in_bytes(value)
|
|
1217
|
+
text_size = len(value)
|
|
862
1218
|
|
|
863
|
-
if not
|
|
864
|
-
|
|
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
|
|
865
1227
|
|
|
866
1228
|
return AnnotatedValue(
|
|
867
|
-
value=
|
|
1229
|
+
value=truncated_value,
|
|
1230
|
+
metadata={
|
|
1231
|
+
"len": byte_size or text_size,
|
|
1232
|
+
"rem": [["!limit", "x", max_length - 3, max_length]],
|
|
1233
|
+
},
|
|
868
1234
|
)
|
|
869
1235
|
|
|
870
1236
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
+
)?
|
|
1271
|
+
)
|
|
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
|
|
875
1287
|
|
|
876
|
-
|
|
1288
|
+
|
|
1289
|
+
def _is_contextvars_broken():
|
|
1290
|
+
# type: () -> bool
|
|
1291
|
+
"""
|
|
1292
|
+
Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
|
|
1293
|
+
"""
|
|
1294
|
+
try:
|
|
1295
|
+
import gevent
|
|
1296
|
+
from gevent.monkey import is_object_patched
|
|
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
|
+
)
|
|
1302
|
+
if is_object_patched("threading", "local"):
|
|
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
|
+
):
|
|
1315
|
+
return False
|
|
1316
|
+
|
|
1317
|
+
return True
|
|
1318
|
+
except ImportError:
|
|
1319
|
+
pass
|
|
1320
|
+
|
|
1321
|
+
try:
|
|
1322
|
+
import greenlet
|
|
1323
|
+
from eventlet.patcher import is_monkey_patched # type: ignore
|
|
1324
|
+
|
|
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):
|
|
1334
|
+
return True
|
|
1335
|
+
except ImportError:
|
|
1336
|
+
pass
|
|
1337
|
+
|
|
1338
|
+
return False
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def _make_threadlocal_contextvars(local):
|
|
1342
|
+
# type: (type) -> type
|
|
1343
|
+
class ContextVar:
|
|
877
1344
|
# Super-limited impl of ContextVar
|
|
878
1345
|
|
|
879
|
-
def __init__(self, name):
|
|
1346
|
+
def __init__(self, name, default=None):
|
|
1347
|
+
# type: (str, Any) -> None
|
|
880
1348
|
self._name = name
|
|
1349
|
+
self._default = default
|
|
881
1350
|
self._local = local()
|
|
1351
|
+
self._original_local = local()
|
|
882
1352
|
|
|
883
|
-
def get(self, default):
|
|
884
|
-
|
|
1353
|
+
def get(self, default=None):
|
|
1354
|
+
# type: (Any) -> Any
|
|
1355
|
+
return getattr(self._local, "value", default or self._default)
|
|
885
1356
|
|
|
886
1357
|
def set(self, value):
|
|
887
|
-
|
|
1358
|
+
# type: (Any) -> Any
|
|
1359
|
+
token = str(random.getrandbits(64))
|
|
1360
|
+
original_value = self.get()
|
|
1361
|
+
setattr(self._original_local, token, original_value)
|
|
1362
|
+
self._local.value = value
|
|
1363
|
+
return token
|
|
888
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]
|
|
889
1370
|
|
|
890
|
-
|
|
891
|
-
|
|
1371
|
+
return ContextVar
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
def _get_contextvars():
|
|
1375
|
+
# type: () -> Tuple[bool, type]
|
|
1376
|
+
"""
|
|
1377
|
+
Figure out the "right" contextvars installation to use. Returns a
|
|
1378
|
+
`contextvars.ContextVar`-like class with a limited API.
|
|
1379
|
+
|
|
1380
|
+
See https://docs.sentry.io/platforms/python/contextvars/ for more information.
|
|
1381
|
+
"""
|
|
1382
|
+
if not _is_contextvars_broken():
|
|
1383
|
+
# aiocontextvars is a PyPI package that ensures that the contextvars
|
|
1384
|
+
# backport (also a PyPI package) works with asyncio under Python 3.6
|
|
1385
|
+
#
|
|
1386
|
+
# Import it if available.
|
|
1387
|
+
if sys.version_info < (3, 7):
|
|
1388
|
+
# `aiocontextvars` is absolutely required for functional
|
|
1389
|
+
# contextvars on Python 3.6.
|
|
1390
|
+
try:
|
|
1391
|
+
from aiocontextvars import ContextVar
|
|
1392
|
+
|
|
1393
|
+
return True, ContextVar
|
|
1394
|
+
except ImportError:
|
|
1395
|
+
pass
|
|
1396
|
+
else:
|
|
1397
|
+
# On Python 3.7 contextvars are functional.
|
|
1398
|
+
try:
|
|
1399
|
+
from contextvars import ContextVar
|
|
1400
|
+
|
|
1401
|
+
return True, ContextVar
|
|
1402
|
+
except ImportError:
|
|
1403
|
+
pass
|
|
1404
|
+
|
|
1405
|
+
# Fall back to basic thread-local usage.
|
|
1406
|
+
|
|
1407
|
+
from threading import local
|
|
1408
|
+
|
|
1409
|
+
return False, _make_threadlocal_contextvars(local)
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
|
|
1413
|
+
|
|
1414
|
+
CONTEXTVARS_ERROR_MESSAGE = """
|
|
1415
|
+
|
|
1416
|
+
With asyncio/ASGI applications, the Sentry SDK requires a functional
|
|
1417
|
+
installation of `contextvars` to avoid leaking scope/context data across
|
|
1418
|
+
requests.
|
|
1419
|
+
|
|
1420
|
+
Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
|
|
1421
|
+
"""
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def qualname_from_function(func):
|
|
1425
|
+
# type: (Callable[..., Any]) -> Optional[str]
|
|
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
|
|
892
1430
|
try:
|
|
893
1431
|
return "%s.%s.%s" % (
|
|
894
|
-
func.im_class.__module__,
|
|
895
|
-
func.im_class.__name__,
|
|
1432
|
+
func.im_class.__module__, # type: ignore
|
|
1433
|
+
func.im_class.__name__, # type: ignore
|
|
896
1434
|
func.__name__,
|
|
897
1435
|
)
|
|
898
1436
|
except Exception:
|
|
899
1437
|
pass
|
|
900
1438
|
|
|
901
|
-
|
|
902
|
-
|
|
1439
|
+
prefix, suffix = "", ""
|
|
1440
|
+
|
|
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
|
|
1453
|
+
|
|
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
|
|
1464
|
+
|
|
1465
|
+
return func_qualname
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def transaction_from_function(func):
|
|
1469
|
+
# type: (Callable[..., Any]) -> Optional[str]
|
|
1470
|
+
return qualname_from_function(func)
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
disable_capture_event = ContextVar("disable_capture_event")
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
class ServerlessTimeoutWarning(Exception): # noqa: N818
|
|
1477
|
+
"""Raised when a serverless method is about to reach its timeout."""
|
|
1478
|
+
|
|
1479
|
+
pass
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
class TimeoutThread(threading.Thread):
|
|
1483
|
+
"""Creates a Thread which runs (sleeps) for a time duration equal to
|
|
1484
|
+
waiting_time and raises a custom ServerlessTimeout exception.
|
|
1485
|
+
"""
|
|
1486
|
+
|
|
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
|
|
1491
|
+
threading.Thread.__init__(self)
|
|
1492
|
+
self.waiting_time = waiting_time
|
|
1493
|
+
self.configured_timeout = configured_timeout
|
|
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
|
+
|
|
1518
|
+
def run(self):
|
|
1519
|
+
# type: () -> None
|
|
1520
|
+
|
|
1521
|
+
self._stop_event.wait(self.waiting_time)
|
|
1522
|
+
|
|
1523
|
+
if self._stop_event.is_set():
|
|
1524
|
+
return
|
|
1525
|
+
|
|
1526
|
+
integer_configured_timeout = int(self.configured_timeout)
|
|
1527
|
+
|
|
1528
|
+
# Setting up the exact integer value of configured time(in seconds)
|
|
1529
|
+
if integer_configured_timeout < self.configured_timeout:
|
|
1530
|
+
integer_configured_timeout = integer_configured_timeout + 1
|
|
1531
|
+
|
|
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
|
+
|
|
1545
|
+
raise ServerlessTimeoutWarning(
|
|
1546
|
+
"WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
|
|
1547
|
+
integer_configured_timeout
|
|
1548
|
+
)
|
|
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
|
+
)
|
|
903
1660
|
)
|
|
904
1661
|
|
|
905
|
-
|
|
906
|
-
|
|
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:
|
|
907
1789
|
return None
|
|
908
1790
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
|
912
1941
|
try:
|
|
913
|
-
|
|
914
|
-
|
|
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:
|
|
915
1948
|
pass
|
|
916
1949
|
|
|
917
|
-
#
|
|
918
|
-
|
|
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
|
+
)
|