sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +81 -19
- sentry_sdk/_types.py +311 -11
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +35 -28
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +20 -11
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +71 -60
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +126 -125
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/crons/api.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
import sentry_sdk
|
|
4
|
+
from sentry_sdk.utils import logger
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from sentry_sdk._types import Event, MonitorConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _create_check_in_event(
|
|
14
|
+
monitor_slug=None, # type: Optional[str]
|
|
15
|
+
check_in_id=None, # type: Optional[str]
|
|
16
|
+
status=None, # type: Optional[str]
|
|
17
|
+
duration_s=None, # type: Optional[float]
|
|
18
|
+
monitor_config=None, # type: Optional[MonitorConfig]
|
|
19
|
+
):
|
|
20
|
+
# type: (...) -> Event
|
|
21
|
+
options = sentry_sdk.get_client().options
|
|
22
|
+
check_in_id = check_in_id or uuid.uuid4().hex # type: str
|
|
23
|
+
|
|
24
|
+
check_in = {
|
|
25
|
+
"type": "check_in",
|
|
26
|
+
"monitor_slug": monitor_slug,
|
|
27
|
+
"check_in_id": check_in_id,
|
|
28
|
+
"status": status,
|
|
29
|
+
"duration": duration_s,
|
|
30
|
+
"environment": options.get("environment", None),
|
|
31
|
+
"release": options.get("release", None),
|
|
32
|
+
} # type: Event
|
|
33
|
+
|
|
34
|
+
if monitor_config:
|
|
35
|
+
check_in["monitor_config"] = monitor_config
|
|
36
|
+
|
|
37
|
+
return check_in
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def capture_checkin(
|
|
41
|
+
monitor_slug=None, # type: Optional[str]
|
|
42
|
+
check_in_id=None, # type: Optional[str]
|
|
43
|
+
status=None, # type: Optional[str]
|
|
44
|
+
duration=None, # type: Optional[float]
|
|
45
|
+
monitor_config=None, # type: Optional[MonitorConfig]
|
|
46
|
+
):
|
|
47
|
+
# type: (...) -> str
|
|
48
|
+
check_in_event = _create_check_in_event(
|
|
49
|
+
monitor_slug=monitor_slug,
|
|
50
|
+
check_in_id=check_in_id,
|
|
51
|
+
status=status,
|
|
52
|
+
duration_s=duration,
|
|
53
|
+
monitor_config=monitor_config,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
sentry_sdk.capture_event(check_in_event)
|
|
57
|
+
|
|
58
|
+
logger.debug(
|
|
59
|
+
f"[Crons] Captured check-in ({check_in_event.get('check_in_id')}): {check_in_event.get('monitor_slug')} -> {check_in_event.get('status')}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return check_in_event["check_in_id"]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from inspect import iscoroutinefunction
|
|
3
|
+
|
|
4
|
+
from sentry_sdk.crons import capture_checkin
|
|
5
|
+
from sentry_sdk.crons.consts import MonitorStatus
|
|
6
|
+
from sentry_sdk.utils import now
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from types import TracebackType
|
|
13
|
+
from typing import (
|
|
14
|
+
Any,
|
|
15
|
+
Optional,
|
|
16
|
+
ParamSpec,
|
|
17
|
+
Type,
|
|
18
|
+
TypeVar,
|
|
19
|
+
Union,
|
|
20
|
+
cast,
|
|
21
|
+
overload,
|
|
22
|
+
)
|
|
23
|
+
from sentry_sdk._types import MonitorConfig
|
|
24
|
+
|
|
25
|
+
P = ParamSpec("P")
|
|
26
|
+
R = TypeVar("R")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class monitor: # noqa: N801
|
|
30
|
+
"""
|
|
31
|
+
Decorator/context manager to capture checkin events for a monitor.
|
|
32
|
+
|
|
33
|
+
Usage (as decorator):
|
|
34
|
+
```
|
|
35
|
+
import sentry_sdk
|
|
36
|
+
|
|
37
|
+
app = Celery()
|
|
38
|
+
|
|
39
|
+
@app.task
|
|
40
|
+
@sentry_sdk.monitor(monitor_slug='my-fancy-slug')
|
|
41
|
+
def test(arg):
|
|
42
|
+
print(arg)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This does not have to be used with Celery, but if you do use it with celery,
|
|
46
|
+
put the `@sentry_sdk.monitor` decorator below Celery's `@app.task` decorator.
|
|
47
|
+
|
|
48
|
+
Usage (as context manager):
|
|
49
|
+
```
|
|
50
|
+
import sentry_sdk
|
|
51
|
+
|
|
52
|
+
def test(arg):
|
|
53
|
+
with sentry_sdk.monitor(monitor_slug='my-fancy-slug'):
|
|
54
|
+
print(arg)
|
|
55
|
+
```
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, monitor_slug=None, monitor_config=None):
|
|
59
|
+
# type: (Optional[str], Optional[MonitorConfig]) -> None
|
|
60
|
+
self.monitor_slug = monitor_slug
|
|
61
|
+
self.monitor_config = monitor_config
|
|
62
|
+
|
|
63
|
+
def __enter__(self):
|
|
64
|
+
# type: () -> None
|
|
65
|
+
self.start_timestamp = now()
|
|
66
|
+
self.check_in_id = capture_checkin(
|
|
67
|
+
monitor_slug=self.monitor_slug,
|
|
68
|
+
status=MonitorStatus.IN_PROGRESS,
|
|
69
|
+
monitor_config=self.monitor_config,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
73
|
+
# type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None
|
|
74
|
+
duration_s = now() - self.start_timestamp
|
|
75
|
+
|
|
76
|
+
if exc_type is None and exc_value is None and traceback is None:
|
|
77
|
+
status = MonitorStatus.OK
|
|
78
|
+
else:
|
|
79
|
+
status = MonitorStatus.ERROR
|
|
80
|
+
|
|
81
|
+
capture_checkin(
|
|
82
|
+
monitor_slug=self.monitor_slug,
|
|
83
|
+
check_in_id=self.check_in_id,
|
|
84
|
+
status=status,
|
|
85
|
+
duration=duration_s,
|
|
86
|
+
monitor_config=self.monitor_config,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if TYPE_CHECKING:
|
|
90
|
+
|
|
91
|
+
@overload
|
|
92
|
+
def __call__(self, fn):
|
|
93
|
+
# type: (Callable[P, Awaitable[Any]]) -> Callable[P, Awaitable[Any]]
|
|
94
|
+
# Unfortunately, mypy does not give us any reliable way to type check the
|
|
95
|
+
# return value of an Awaitable (i.e. async function) for this overload,
|
|
96
|
+
# since calling iscouroutinefunction narrows the type to Callable[P, Awaitable[Any]].
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
@overload
|
|
100
|
+
def __call__(self, fn):
|
|
101
|
+
# type: (Callable[P, R]) -> Callable[P, R]
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def __call__(
|
|
105
|
+
self,
|
|
106
|
+
fn, # type: Union[Callable[P, R], Callable[P, Awaitable[Any]]]
|
|
107
|
+
):
|
|
108
|
+
# type: (...) -> Union[Callable[P, R], Callable[P, Awaitable[Any]]]
|
|
109
|
+
if iscoroutinefunction(fn):
|
|
110
|
+
return self._async_wrapper(fn)
|
|
111
|
+
|
|
112
|
+
else:
|
|
113
|
+
if TYPE_CHECKING:
|
|
114
|
+
fn = cast("Callable[P, R]", fn)
|
|
115
|
+
return self._sync_wrapper(fn)
|
|
116
|
+
|
|
117
|
+
def _async_wrapper(self, fn):
|
|
118
|
+
# type: (Callable[P, Awaitable[Any]]) -> Callable[P, Awaitable[Any]]
|
|
119
|
+
@wraps(fn)
|
|
120
|
+
async def inner(*args: "P.args", **kwargs: "P.kwargs"):
|
|
121
|
+
# type: (...) -> R
|
|
122
|
+
with self:
|
|
123
|
+
return await fn(*args, **kwargs)
|
|
124
|
+
|
|
125
|
+
return inner
|
|
126
|
+
|
|
127
|
+
def _sync_wrapper(self, fn):
|
|
128
|
+
# type: (Callable[P, R]) -> Callable[P, R]
|
|
129
|
+
@wraps(fn)
|
|
130
|
+
def inner(*args: "P.args", **kwargs: "P.kwargs"):
|
|
131
|
+
# type: (...) -> R
|
|
132
|
+
with self:
|
|
133
|
+
return fn(*args, **kwargs)
|
|
134
|
+
|
|
135
|
+
return inner
|
sentry_sdk/debug.py
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import logging
|
|
3
|
+
import warnings
|
|
3
4
|
|
|
4
|
-
from sentry_sdk import
|
|
5
|
-
from sentry_sdk.hub import Hub
|
|
6
|
-
from sentry_sdk.utils import logger
|
|
5
|
+
from sentry_sdk import get_client
|
|
7
6
|
from sentry_sdk.client import _client_init_debug
|
|
7
|
+
from sentry_sdk.utils import logger
|
|
8
8
|
from logging import LogRecord
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
11
|
+
class _DebugFilter(logging.Filter):
|
|
12
12
|
def filter(self, record):
|
|
13
13
|
# type: (LogRecord) -> bool
|
|
14
14
|
if _client_init_debug.get(False):
|
|
15
15
|
return True
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return hub.client.options["debug"]
|
|
19
|
-
return False
|
|
16
|
+
|
|
17
|
+
return get_client().options["debug"]
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
def init_debug_support():
|
|
23
21
|
# type: () -> None
|
|
24
22
|
if not logger.handlers:
|
|
25
23
|
configure_logger()
|
|
26
|
-
configure_debug_hub()
|
|
27
24
|
|
|
28
25
|
|
|
29
26
|
def configure_logger():
|
|
@@ -32,13 +29,13 @@ def configure_logger():
|
|
|
32
29
|
_handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s"))
|
|
33
30
|
logger.addHandler(_handler)
|
|
34
31
|
logger.setLevel(logging.DEBUG)
|
|
35
|
-
logger.addFilter(
|
|
32
|
+
logger.addFilter(_DebugFilter())
|
|
36
33
|
|
|
37
34
|
|
|
38
35
|
def configure_debug_hub():
|
|
39
36
|
# type: () -> None
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
warnings.warn(
|
|
38
|
+
"configure_debug_hub is deprecated. Please remove calls to it, as it is a no-op.",
|
|
39
|
+
DeprecationWarning,
|
|
40
|
+
stacklevel=2,
|
|
41
|
+
)
|
sentry_sdk/envelope.py
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import json
|
|
3
|
-
import shutil
|
|
4
3
|
import mimetypes
|
|
5
4
|
|
|
6
|
-
from sentry_sdk.
|
|
7
|
-
from sentry_sdk.
|
|
8
|
-
from sentry_sdk.sessions import Session
|
|
9
|
-
from sentry_sdk.tracing import Transaction
|
|
10
|
-
from sentry_sdk.utils import json_dumps
|
|
5
|
+
from sentry_sdk.session import Session
|
|
6
|
+
from sentry_sdk.utils import json_dumps, capture_internal_exceptions
|
|
11
7
|
|
|
12
|
-
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
13
11
|
from typing import Any
|
|
14
|
-
from typing import Tuple
|
|
15
12
|
from typing import Optional
|
|
16
13
|
from typing import Union
|
|
17
14
|
from typing import Dict
|
|
@@ -21,10 +18,24 @@ if MYPY:
|
|
|
21
18
|
from sentry_sdk._types import Event, EventDataCategory
|
|
22
19
|
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
def parse_json(data):
|
|
22
|
+
# type: (Union[bytes, str]) -> Any
|
|
23
|
+
# on some python 3 versions this needs to be bytes
|
|
24
|
+
if isinstance(data, bytes):
|
|
25
|
+
data = data.decode("utf-8", "replace")
|
|
26
|
+
return json.loads(data)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Envelope:
|
|
30
|
+
"""
|
|
31
|
+
Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints
|
|
32
|
+
documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular,
|
|
33
|
+
each envelope may have at most one Item with type "event" or "transaction" (but not both).
|
|
34
|
+
"""
|
|
35
|
+
|
|
25
36
|
def __init__(
|
|
26
37
|
self,
|
|
27
|
-
headers=None, # type: Optional[Dict[str,
|
|
38
|
+
headers=None, # type: Optional[Dict[str, Any]]
|
|
28
39
|
items=None, # type: Optional[List[Item]]
|
|
29
40
|
):
|
|
30
41
|
# type: (...) -> None
|
|
@@ -46,27 +57,65 @@ class Envelope(object):
|
|
|
46
57
|
)
|
|
47
58
|
|
|
48
59
|
def add_event(
|
|
49
|
-
self,
|
|
60
|
+
self,
|
|
61
|
+
event, # type: Event
|
|
50
62
|
):
|
|
51
63
|
# type: (...) -> None
|
|
52
64
|
self.add_item(Item(payload=PayloadRef(json=event), type="event"))
|
|
53
65
|
|
|
54
66
|
def add_transaction(
|
|
55
|
-
self,
|
|
67
|
+
self,
|
|
68
|
+
transaction, # type: Event
|
|
56
69
|
):
|
|
57
70
|
# type: (...) -> None
|
|
58
71
|
self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))
|
|
59
72
|
|
|
73
|
+
def add_profile(
|
|
74
|
+
self,
|
|
75
|
+
profile, # type: Any
|
|
76
|
+
):
|
|
77
|
+
# type: (...) -> None
|
|
78
|
+
self.add_item(Item(payload=PayloadRef(json=profile), type="profile"))
|
|
79
|
+
|
|
80
|
+
def add_profile_chunk(
|
|
81
|
+
self,
|
|
82
|
+
profile_chunk, # type: Any
|
|
83
|
+
):
|
|
84
|
+
# type: (...) -> None
|
|
85
|
+
self.add_item(
|
|
86
|
+
Item(
|
|
87
|
+
payload=PayloadRef(json=profile_chunk),
|
|
88
|
+
type="profile_chunk",
|
|
89
|
+
headers={"platform": profile_chunk.get("platform", "python")},
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def add_checkin(
|
|
94
|
+
self,
|
|
95
|
+
checkin, # type: Any
|
|
96
|
+
):
|
|
97
|
+
# type: (...) -> None
|
|
98
|
+
self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in"))
|
|
99
|
+
|
|
60
100
|
def add_session(
|
|
61
|
-
self,
|
|
101
|
+
self,
|
|
102
|
+
session, # type: Union[Session, Any]
|
|
62
103
|
):
|
|
63
104
|
# type: (...) -> None
|
|
64
105
|
if isinstance(session, Session):
|
|
65
106
|
session = session.to_json()
|
|
66
107
|
self.add_item(Item(payload=PayloadRef(json=session), type="session"))
|
|
67
108
|
|
|
109
|
+
def add_sessions(
|
|
110
|
+
self,
|
|
111
|
+
sessions, # type: Any
|
|
112
|
+
):
|
|
113
|
+
# type: (...) -> None
|
|
114
|
+
self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
|
|
115
|
+
|
|
68
116
|
def add_item(
|
|
69
|
-
self,
|
|
117
|
+
self,
|
|
118
|
+
item, # type: Item
|
|
70
119
|
):
|
|
71
120
|
# type: (...) -> None
|
|
72
121
|
self.items.append(item)
|
|
@@ -92,7 +141,8 @@ class Envelope(object):
|
|
|
92
141
|
return iter(self.items)
|
|
93
142
|
|
|
94
143
|
def serialize_into(
|
|
95
|
-
self,
|
|
144
|
+
self,
|
|
145
|
+
f, # type: Any
|
|
96
146
|
):
|
|
97
147
|
# type: (...) -> None
|
|
98
148
|
f.write(json_dumps(self.headers))
|
|
@@ -108,10 +158,11 @@ class Envelope(object):
|
|
|
108
158
|
|
|
109
159
|
@classmethod
|
|
110
160
|
def deserialize_from(
|
|
111
|
-
cls,
|
|
161
|
+
cls,
|
|
162
|
+
f, # type: Any
|
|
112
163
|
):
|
|
113
164
|
# type: (...) -> Envelope
|
|
114
|
-
headers =
|
|
165
|
+
headers = parse_json(f.readline())
|
|
115
166
|
items = []
|
|
116
167
|
while 1:
|
|
117
168
|
item = Item.deserialize_from(f)
|
|
@@ -122,7 +173,8 @@ class Envelope(object):
|
|
|
122
173
|
|
|
123
174
|
@classmethod
|
|
124
175
|
def deserialize(
|
|
125
|
-
cls,
|
|
176
|
+
cls,
|
|
177
|
+
bytes, # type: bytes
|
|
126
178
|
):
|
|
127
179
|
# type: (...) -> Envelope
|
|
128
180
|
return cls.deserialize_from(io.BytesIO(bytes))
|
|
@@ -132,11 +184,11 @@ class Envelope(object):
|
|
|
132
184
|
return "<Envelope headers=%r items=%r>" % (self.headers, self.items)
|
|
133
185
|
|
|
134
186
|
|
|
135
|
-
class PayloadRef
|
|
187
|
+
class PayloadRef:
|
|
136
188
|
def __init__(
|
|
137
189
|
self,
|
|
138
190
|
bytes=None, # type: Optional[bytes]
|
|
139
|
-
path=None, # type: Optional[Union[bytes,
|
|
191
|
+
path=None, # type: Optional[Union[bytes, str]]
|
|
140
192
|
json=None, # type: Optional[Any]
|
|
141
193
|
):
|
|
142
194
|
# type: (...) -> None
|
|
@@ -148,33 +200,12 @@ class PayloadRef(object):
|
|
|
148
200
|
# type: (...) -> bytes
|
|
149
201
|
if self.bytes is None:
|
|
150
202
|
if self.path is not None:
|
|
151
|
-
with
|
|
152
|
-
self.
|
|
203
|
+
with capture_internal_exceptions():
|
|
204
|
+
with open(self.path, "rb") as f:
|
|
205
|
+
self.bytes = f.read()
|
|
153
206
|
elif self.json is not None:
|
|
154
207
|
self.bytes = json_dumps(self.json)
|
|
155
|
-
|
|
156
|
-
self.bytes = b""
|
|
157
|
-
return self.bytes
|
|
158
|
-
|
|
159
|
-
def _prepare_serialize(self):
|
|
160
|
-
# type: (...) -> Tuple[Any, Any]
|
|
161
|
-
if self.path is not None and self.bytes is None:
|
|
162
|
-
f = open(self.path, "rb")
|
|
163
|
-
f.seek(0, 2)
|
|
164
|
-
length = f.tell()
|
|
165
|
-
f.seek(0, 0)
|
|
166
|
-
|
|
167
|
-
def writer(out):
|
|
168
|
-
# type: (Any) -> None
|
|
169
|
-
try:
|
|
170
|
-
shutil.copyfileobj(f, out)
|
|
171
|
-
finally:
|
|
172
|
-
f.close()
|
|
173
|
-
|
|
174
|
-
return length, writer
|
|
175
|
-
|
|
176
|
-
bytes = self.get_bytes()
|
|
177
|
-
return len(bytes), lambda f: f.write(bytes)
|
|
208
|
+
return self.bytes or b""
|
|
178
209
|
|
|
179
210
|
@property
|
|
180
211
|
def inferred_content_type(self):
|
|
@@ -195,11 +226,11 @@ class PayloadRef(object):
|
|
|
195
226
|
return "<Payload %r>" % (self.inferred_content_type,)
|
|
196
227
|
|
|
197
228
|
|
|
198
|
-
class Item
|
|
229
|
+
class Item:
|
|
199
230
|
def __init__(
|
|
200
231
|
self,
|
|
201
|
-
payload, # type: Union[bytes,
|
|
202
|
-
headers=None, # type: Optional[Dict[str,
|
|
232
|
+
payload, # type: Union[bytes, str, PayloadRef]
|
|
233
|
+
headers=None, # type: Optional[Dict[str, Any]]
|
|
203
234
|
type=None, # type: Optional[str]
|
|
204
235
|
content_type=None, # type: Optional[str]
|
|
205
236
|
filename=None, # type: Optional[str]
|
|
@@ -211,7 +242,7 @@ class Item(object):
|
|
|
211
242
|
self.headers = headers
|
|
212
243
|
if isinstance(payload, bytes):
|
|
213
244
|
payload = PayloadRef(bytes=payload)
|
|
214
|
-
elif isinstance(payload,
|
|
245
|
+
elif isinstance(payload, str):
|
|
215
246
|
payload = PayloadRef(bytes=payload.encode("utf-8"))
|
|
216
247
|
else:
|
|
217
248
|
payload = payload
|
|
@@ -244,7 +275,7 @@ class Item(object):
|
|
|
244
275
|
def data_category(self):
|
|
245
276
|
# type: (...) -> EventDataCategory
|
|
246
277
|
ty = self.headers.get("type")
|
|
247
|
-
if ty == "session":
|
|
278
|
+
if ty == "session" or ty == "sessions":
|
|
248
279
|
return "session"
|
|
249
280
|
elif ty == "attachment":
|
|
250
281
|
return "attachment"
|
|
@@ -252,6 +283,18 @@ class Item(object):
|
|
|
252
283
|
return "transaction"
|
|
253
284
|
elif ty == "event":
|
|
254
285
|
return "error"
|
|
286
|
+
elif ty == "log":
|
|
287
|
+
return "log_item"
|
|
288
|
+
elif ty == "trace_metric":
|
|
289
|
+
return "trace_metric"
|
|
290
|
+
elif ty == "client_report":
|
|
291
|
+
return "internal"
|
|
292
|
+
elif ty == "profile":
|
|
293
|
+
return "profile"
|
|
294
|
+
elif ty == "profile_chunk":
|
|
295
|
+
return "profile_chunk"
|
|
296
|
+
elif ty == "check_in":
|
|
297
|
+
return "monitor"
|
|
255
298
|
else:
|
|
256
299
|
return "default"
|
|
257
300
|
|
|
@@ -275,15 +318,16 @@ class Item(object):
|
|
|
275
318
|
return None
|
|
276
319
|
|
|
277
320
|
def serialize_into(
|
|
278
|
-
self,
|
|
321
|
+
self,
|
|
322
|
+
f, # type: Any
|
|
279
323
|
):
|
|
280
324
|
# type: (...) -> None
|
|
281
325
|
headers = dict(self.headers)
|
|
282
|
-
|
|
283
|
-
headers["length"] =
|
|
326
|
+
bytes = self.get_bytes()
|
|
327
|
+
headers["length"] = len(bytes)
|
|
284
328
|
f.write(json_dumps(headers))
|
|
285
329
|
f.write(b"\n")
|
|
286
|
-
|
|
330
|
+
f.write(bytes)
|
|
287
331
|
f.write(b"\n")
|
|
288
332
|
|
|
289
333
|
def serialize(self):
|
|
@@ -294,25 +338,32 @@ class Item(object):
|
|
|
294
338
|
|
|
295
339
|
@classmethod
|
|
296
340
|
def deserialize_from(
|
|
297
|
-
cls,
|
|
341
|
+
cls,
|
|
342
|
+
f, # type: Any
|
|
298
343
|
):
|
|
299
344
|
# type: (...) -> Optional[Item]
|
|
300
345
|
line = f.readline().rstrip()
|
|
301
346
|
if not line:
|
|
302
347
|
return None
|
|
303
|
-
headers =
|
|
304
|
-
length = headers
|
|
305
|
-
|
|
348
|
+
headers = parse_json(line)
|
|
349
|
+
length = headers.get("length")
|
|
350
|
+
if length is not None:
|
|
351
|
+
payload = f.read(length)
|
|
352
|
+
f.readline()
|
|
353
|
+
else:
|
|
354
|
+
# if no length was specified we need to read up to the end of line
|
|
355
|
+
# and remove it (if it is present, i.e. not the very last char in an eof terminated envelope)
|
|
356
|
+
payload = f.readline().rstrip(b"\n")
|
|
306
357
|
if headers.get("type") in ("event", "transaction"):
|
|
307
|
-
rv = cls(headers=headers, payload=PayloadRef(json=
|
|
358
|
+
rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
|
|
308
359
|
else:
|
|
309
360
|
rv = cls(headers=headers, payload=payload)
|
|
310
|
-
f.readline()
|
|
311
361
|
return rv
|
|
312
362
|
|
|
313
363
|
@classmethod
|
|
314
364
|
def deserialize(
|
|
315
|
-
cls,
|
|
365
|
+
cls,
|
|
366
|
+
bytes, # type: bytes
|
|
316
367
|
):
|
|
317
368
|
# type: (...) -> Optional[Item]
|
|
318
369
|
return cls.deserialize_from(io.BytesIO(bytes))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import sentry_sdk
|
|
3
|
+
from sentry_sdk._lru_cache import LRUCache
|
|
4
|
+
from threading import Lock
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import TypedDict
|
|
10
|
+
|
|
11
|
+
FlagData = TypedDict("FlagData", {"flag": str, "result": bool})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_FLAG_CAPACITY = 100
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FlagBuffer:
|
|
18
|
+
def __init__(self, capacity):
|
|
19
|
+
# type: (int) -> None
|
|
20
|
+
self.capacity = capacity
|
|
21
|
+
self.lock = Lock()
|
|
22
|
+
|
|
23
|
+
# Buffer is private. The name is mangled to discourage use. If you use this attribute
|
|
24
|
+
# directly you're on your own!
|
|
25
|
+
self.__buffer = LRUCache(capacity)
|
|
26
|
+
|
|
27
|
+
def clear(self):
|
|
28
|
+
# type: () -> None
|
|
29
|
+
self.__buffer = LRUCache(self.capacity)
|
|
30
|
+
|
|
31
|
+
def __deepcopy__(self, memo):
|
|
32
|
+
# type: (dict[int, Any]) -> FlagBuffer
|
|
33
|
+
with self.lock:
|
|
34
|
+
buffer = FlagBuffer(self.capacity)
|
|
35
|
+
buffer.__buffer = copy.deepcopy(self.__buffer, memo)
|
|
36
|
+
return buffer
|
|
37
|
+
|
|
38
|
+
def get(self):
|
|
39
|
+
# type: () -> list[FlagData]
|
|
40
|
+
with self.lock:
|
|
41
|
+
return [
|
|
42
|
+
{"flag": key, "result": value} for key, value in self.__buffer.get_all()
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def set(self, flag, result):
|
|
46
|
+
# type: (str, bool) -> None
|
|
47
|
+
if isinstance(result, FlagBuffer):
|
|
48
|
+
# If someone were to insert `self` into `self` this would create a circular dependency
|
|
49
|
+
# on the lock. This is of course a deadlock. However, this is far outside the expected
|
|
50
|
+
# usage of this class. We guard against it here for completeness and to document this
|
|
51
|
+
# expected failure mode.
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"FlagBuffer instances can not be inserted into the dictionary."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
with self.lock:
|
|
57
|
+
self.__buffer.set(flag, result)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def add_feature_flag(flag, result):
|
|
61
|
+
# type: (str, bool) -> None
|
|
62
|
+
"""
|
|
63
|
+
Records a flag and its value to be sent on subsequent error events.
|
|
64
|
+
We recommend you do this on flag evaluations. Flags are buffered per Sentry scope.
|
|
65
|
+
"""
|
|
66
|
+
flags = sentry_sdk.get_isolation_scope().flags
|
|
67
|
+
flags.set(flag, result)
|
|
68
|
+
|
|
69
|
+
span = sentry_sdk.get_current_span()
|
|
70
|
+
if span:
|
|
71
|
+
span.set_flag(f"flag.evaluation.{flag}", result)
|