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/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,40 +1,41 @@
|
|
|
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():
|
|
21
|
+
# type: () -> None
|
|
23
22
|
if not logger.handlers:
|
|
24
23
|
configure_logger()
|
|
25
|
-
configure_debug_hub()
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
def configure_logger():
|
|
27
|
+
# type: () -> None
|
|
29
28
|
_handler = logging.StreamHandler(sys.stderr)
|
|
30
29
|
_handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s"))
|
|
31
30
|
logger.addHandler(_handler)
|
|
32
31
|
logger.setLevel(logging.DEBUG)
|
|
33
|
-
logger.addFilter(
|
|
32
|
+
logger.addFilter(_DebugFilter())
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
def configure_debug_hub():
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
# type: () -> None
|
|
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
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import mimetypes
|
|
4
|
+
|
|
5
|
+
from sentry_sdk.session import Session
|
|
6
|
+
from sentry_sdk.utils import json_dumps, capture_internal_exceptions
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Any
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from typing import Union
|
|
14
|
+
from typing import Dict
|
|
15
|
+
from typing import List
|
|
16
|
+
from typing import Iterator
|
|
17
|
+
|
|
18
|
+
from sentry_sdk._types import Event, EventDataCategory
|
|
19
|
+
|
|
20
|
+
|
|
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
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
headers=None, # type: Optional[Dict[str, Any]]
|
|
39
|
+
items=None, # type: Optional[List[Item]]
|
|
40
|
+
):
|
|
41
|
+
# type: (...) -> None
|
|
42
|
+
if headers is not None:
|
|
43
|
+
headers = dict(headers)
|
|
44
|
+
self.headers = headers or {}
|
|
45
|
+
if items is None:
|
|
46
|
+
items = []
|
|
47
|
+
else:
|
|
48
|
+
items = list(items)
|
|
49
|
+
self.items = items
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def description(self):
|
|
53
|
+
# type: (...) -> str
|
|
54
|
+
return "envelope with %s items (%s)" % (
|
|
55
|
+
len(self.items),
|
|
56
|
+
", ".join(x.data_category for x in self.items),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def add_event(
|
|
60
|
+
self,
|
|
61
|
+
event, # type: Event
|
|
62
|
+
):
|
|
63
|
+
# type: (...) -> None
|
|
64
|
+
self.add_item(Item(payload=PayloadRef(json=event), type="event"))
|
|
65
|
+
|
|
66
|
+
def add_transaction(
|
|
67
|
+
self,
|
|
68
|
+
transaction, # type: Event
|
|
69
|
+
):
|
|
70
|
+
# type: (...) -> None
|
|
71
|
+
self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))
|
|
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
|
+
|
|
100
|
+
def add_session(
|
|
101
|
+
self,
|
|
102
|
+
session, # type: Union[Session, Any]
|
|
103
|
+
):
|
|
104
|
+
# type: (...) -> None
|
|
105
|
+
if isinstance(session, Session):
|
|
106
|
+
session = session.to_json()
|
|
107
|
+
self.add_item(Item(payload=PayloadRef(json=session), type="session"))
|
|
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
|
+
|
|
116
|
+
def add_item(
|
|
117
|
+
self,
|
|
118
|
+
item, # type: Item
|
|
119
|
+
):
|
|
120
|
+
# type: (...) -> None
|
|
121
|
+
self.items.append(item)
|
|
122
|
+
|
|
123
|
+
def get_event(self):
|
|
124
|
+
# type: (...) -> Optional[Event]
|
|
125
|
+
for items in self.items:
|
|
126
|
+
event = items.get_event()
|
|
127
|
+
if event is not None:
|
|
128
|
+
return event
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def get_transaction_event(self):
|
|
132
|
+
# type: (...) -> Optional[Event]
|
|
133
|
+
for item in self.items:
|
|
134
|
+
event = item.get_transaction_event()
|
|
135
|
+
if event is not None:
|
|
136
|
+
return event
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def __iter__(self):
|
|
140
|
+
# type: (...) -> Iterator[Item]
|
|
141
|
+
return iter(self.items)
|
|
142
|
+
|
|
143
|
+
def serialize_into(
|
|
144
|
+
self,
|
|
145
|
+
f, # type: Any
|
|
146
|
+
):
|
|
147
|
+
# type: (...) -> None
|
|
148
|
+
f.write(json_dumps(self.headers))
|
|
149
|
+
f.write(b"\n")
|
|
150
|
+
for item in self.items:
|
|
151
|
+
item.serialize_into(f)
|
|
152
|
+
|
|
153
|
+
def serialize(self):
|
|
154
|
+
# type: (...) -> bytes
|
|
155
|
+
out = io.BytesIO()
|
|
156
|
+
self.serialize_into(out)
|
|
157
|
+
return out.getvalue()
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def deserialize_from(
|
|
161
|
+
cls,
|
|
162
|
+
f, # type: Any
|
|
163
|
+
):
|
|
164
|
+
# type: (...) -> Envelope
|
|
165
|
+
headers = parse_json(f.readline())
|
|
166
|
+
items = []
|
|
167
|
+
while 1:
|
|
168
|
+
item = Item.deserialize_from(f)
|
|
169
|
+
if item is None:
|
|
170
|
+
break
|
|
171
|
+
items.append(item)
|
|
172
|
+
return cls(headers=headers, items=items)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def deserialize(
|
|
176
|
+
cls,
|
|
177
|
+
bytes, # type: bytes
|
|
178
|
+
):
|
|
179
|
+
# type: (...) -> Envelope
|
|
180
|
+
return cls.deserialize_from(io.BytesIO(bytes))
|
|
181
|
+
|
|
182
|
+
def __repr__(self):
|
|
183
|
+
# type: (...) -> str
|
|
184
|
+
return "<Envelope headers=%r items=%r>" % (self.headers, self.items)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class PayloadRef:
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
bytes=None, # type: Optional[bytes]
|
|
191
|
+
path=None, # type: Optional[Union[bytes, str]]
|
|
192
|
+
json=None, # type: Optional[Any]
|
|
193
|
+
):
|
|
194
|
+
# type: (...) -> None
|
|
195
|
+
self.json = json
|
|
196
|
+
self.bytes = bytes
|
|
197
|
+
self.path = path
|
|
198
|
+
|
|
199
|
+
def get_bytes(self):
|
|
200
|
+
# type: (...) -> bytes
|
|
201
|
+
if self.bytes is None:
|
|
202
|
+
if self.path is not None:
|
|
203
|
+
with capture_internal_exceptions():
|
|
204
|
+
with open(self.path, "rb") as f:
|
|
205
|
+
self.bytes = f.read()
|
|
206
|
+
elif self.json is not None:
|
|
207
|
+
self.bytes = json_dumps(self.json)
|
|
208
|
+
return self.bytes or b""
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def inferred_content_type(self):
|
|
212
|
+
# type: (...) -> str
|
|
213
|
+
if self.json is not None:
|
|
214
|
+
return "application/json"
|
|
215
|
+
elif self.path is not None:
|
|
216
|
+
path = self.path
|
|
217
|
+
if isinstance(path, bytes):
|
|
218
|
+
path = path.decode("utf-8", "replace")
|
|
219
|
+
ty = mimetypes.guess_type(path)[0]
|
|
220
|
+
if ty:
|
|
221
|
+
return ty
|
|
222
|
+
return "application/octet-stream"
|
|
223
|
+
|
|
224
|
+
def __repr__(self):
|
|
225
|
+
# type: (...) -> str
|
|
226
|
+
return "<Payload %r>" % (self.inferred_content_type,)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Item:
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
payload, # type: Union[bytes, str, PayloadRef]
|
|
233
|
+
headers=None, # type: Optional[Dict[str, Any]]
|
|
234
|
+
type=None, # type: Optional[str]
|
|
235
|
+
content_type=None, # type: Optional[str]
|
|
236
|
+
filename=None, # type: Optional[str]
|
|
237
|
+
):
|
|
238
|
+
if headers is not None:
|
|
239
|
+
headers = dict(headers)
|
|
240
|
+
elif headers is None:
|
|
241
|
+
headers = {}
|
|
242
|
+
self.headers = headers
|
|
243
|
+
if isinstance(payload, bytes):
|
|
244
|
+
payload = PayloadRef(bytes=payload)
|
|
245
|
+
elif isinstance(payload, str):
|
|
246
|
+
payload = PayloadRef(bytes=payload.encode("utf-8"))
|
|
247
|
+
else:
|
|
248
|
+
payload = payload
|
|
249
|
+
|
|
250
|
+
if filename is not None:
|
|
251
|
+
headers["filename"] = filename
|
|
252
|
+
if type is not None:
|
|
253
|
+
headers["type"] = type
|
|
254
|
+
if content_type is not None:
|
|
255
|
+
headers["content_type"] = content_type
|
|
256
|
+
elif "content_type" not in headers:
|
|
257
|
+
headers["content_type"] = payload.inferred_content_type
|
|
258
|
+
|
|
259
|
+
self.payload = payload
|
|
260
|
+
|
|
261
|
+
def __repr__(self):
|
|
262
|
+
# type: (...) -> str
|
|
263
|
+
return "<Item headers=%r payload=%r data_category=%r>" % (
|
|
264
|
+
self.headers,
|
|
265
|
+
self.payload,
|
|
266
|
+
self.data_category,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def type(self):
|
|
271
|
+
# type: (...) -> Optional[str]
|
|
272
|
+
return self.headers.get("type")
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def data_category(self):
|
|
276
|
+
# type: (...) -> EventDataCategory
|
|
277
|
+
ty = self.headers.get("type")
|
|
278
|
+
if ty == "session" or ty == "sessions":
|
|
279
|
+
return "session"
|
|
280
|
+
elif ty == "attachment":
|
|
281
|
+
return "attachment"
|
|
282
|
+
elif ty == "transaction":
|
|
283
|
+
return "transaction"
|
|
284
|
+
elif ty == "event":
|
|
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"
|
|
298
|
+
else:
|
|
299
|
+
return "default"
|
|
300
|
+
|
|
301
|
+
def get_bytes(self):
|
|
302
|
+
# type: (...) -> bytes
|
|
303
|
+
return self.payload.get_bytes()
|
|
304
|
+
|
|
305
|
+
def get_event(self):
|
|
306
|
+
# type: (...) -> Optional[Event]
|
|
307
|
+
"""
|
|
308
|
+
Returns an error event if there is one.
|
|
309
|
+
"""
|
|
310
|
+
if self.type == "event" and self.payload.json is not None:
|
|
311
|
+
return self.payload.json
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
def get_transaction_event(self):
|
|
315
|
+
# type: (...) -> Optional[Event]
|
|
316
|
+
if self.type == "transaction" and self.payload.json is not None:
|
|
317
|
+
return self.payload.json
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
def serialize_into(
|
|
321
|
+
self,
|
|
322
|
+
f, # type: Any
|
|
323
|
+
):
|
|
324
|
+
# type: (...) -> None
|
|
325
|
+
headers = dict(self.headers)
|
|
326
|
+
bytes = self.get_bytes()
|
|
327
|
+
headers["length"] = len(bytes)
|
|
328
|
+
f.write(json_dumps(headers))
|
|
329
|
+
f.write(b"\n")
|
|
330
|
+
f.write(bytes)
|
|
331
|
+
f.write(b"\n")
|
|
332
|
+
|
|
333
|
+
def serialize(self):
|
|
334
|
+
# type: (...) -> bytes
|
|
335
|
+
out = io.BytesIO()
|
|
336
|
+
self.serialize_into(out)
|
|
337
|
+
return out.getvalue()
|
|
338
|
+
|
|
339
|
+
@classmethod
|
|
340
|
+
def deserialize_from(
|
|
341
|
+
cls,
|
|
342
|
+
f, # type: Any
|
|
343
|
+
):
|
|
344
|
+
# type: (...) -> Optional[Item]
|
|
345
|
+
line = f.readline().rstrip()
|
|
346
|
+
if not line:
|
|
347
|
+
return None
|
|
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")
|
|
357
|
+
if headers.get("type") in ("event", "transaction"):
|
|
358
|
+
rv = cls(headers=headers, payload=PayloadRef(json=parse_json(payload)))
|
|
359
|
+
else:
|
|
360
|
+
rv = cls(headers=headers, payload=payload)
|
|
361
|
+
return rv
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def deserialize(
|
|
365
|
+
cls,
|
|
366
|
+
bytes, # type: bytes
|
|
367
|
+
):
|
|
368
|
+
# type: (...) -> Optional[Item]
|
|
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)
|