blaxel 0.2.32__py3-none-any.whl → 0.2.34__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.
- blaxel/__init__.py +2 -2
- blaxel/core/__init__.py +2 -1
- blaxel/core/common/__init__.py +5 -1
- blaxel/core/common/autoload.py +2 -127
- blaxel/core/common/sentry.py +319 -0
- blaxel/core/sandbox/default/process.py +144 -29
- blaxel/core/sandbox/default/sandbox.py +34 -10
- blaxel/core/sandbox/sync/process.py +150 -24
- blaxel/core/sandbox/sync/sandbox.py +34 -10
- blaxel/core/sandbox/types.py +5 -0
- blaxel/core/volume/__init__.py +2 -2
- blaxel/core/volume/volume.py +200 -6
- {blaxel-0.2.32.dist-info → blaxel-0.2.34.dist-info}/METADATA +1 -2
- {blaxel-0.2.32.dist-info → blaxel-0.2.34.dist-info}/RECORD +16 -15
- {blaxel-0.2.32.dist-info → blaxel-0.2.34.dist-info}/WHEEL +0 -0
- {blaxel-0.2.32.dist-info → blaxel-0.2.34.dist-info}/licenses/LICENSE +0 -0
blaxel/__init__.py
CHANGED
|
@@ -4,8 +4,8 @@ from .core.common.autoload import autoload
|
|
|
4
4
|
from .core.common.env import env
|
|
5
5
|
from .core.common.settings import settings
|
|
6
6
|
|
|
7
|
-
__version__ = "0.2.
|
|
8
|
-
__commit__ = "
|
|
7
|
+
__version__ = "0.2.34"
|
|
8
|
+
__commit__ = "84fc1c14e48dec727c7de1966c51022e09f7c80f"
|
|
9
9
|
__sentry_dsn__ = "https://9711de13cd02b285ca4378c01de8dc30@o4508714045276160.ingest.us.sentry.io/4510461121462272"
|
|
10
10
|
__all__ = ["autoload", "settings", "env"]
|
|
11
11
|
|
blaxel/core/__init__.py
CHANGED
|
@@ -30,7 +30,7 @@ from .sandbox import (
|
|
|
30
30
|
)
|
|
31
31
|
from .sandbox.types import Sandbox
|
|
32
32
|
from .tools import BlTools, bl_tools, convert_mcp_tool_to_blaxel_tool
|
|
33
|
-
from .volume import VolumeCreateConfiguration, VolumeInstance
|
|
33
|
+
from .volume import SyncVolumeInstance, VolumeCreateConfiguration, VolumeInstance
|
|
34
34
|
|
|
35
35
|
__all__ = [
|
|
36
36
|
"BlAgent",
|
|
@@ -65,6 +65,7 @@ __all__ = [
|
|
|
65
65
|
"convert_mcp_tool_to_blaxel_tool",
|
|
66
66
|
"websocket_client",
|
|
67
67
|
"VolumeInstance",
|
|
68
|
+
"SyncVolumeInstance",
|
|
68
69
|
"VolumeCreateConfiguration",
|
|
69
70
|
"verify_webhook_signature",
|
|
70
71
|
"verify_webhook_from_request",
|
blaxel/core/common/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from .autoload import autoload
|
|
1
|
+
from .autoload import autoload
|
|
2
2
|
from .env import env
|
|
3
|
+
from .sentry import capture_exception, flush_sentry, init_sentry, is_sentry_initialized
|
|
3
4
|
from .internal import get_alphanumeric_limited_hash, get_global_unique_hash
|
|
4
5
|
from .settings import Settings, settings
|
|
5
6
|
from .webhook import (
|
|
@@ -12,6 +13,9 @@ from .webhook import (
|
|
|
12
13
|
__all__ = [
|
|
13
14
|
"autoload",
|
|
14
15
|
"capture_exception",
|
|
16
|
+
"flush_sentry",
|
|
17
|
+
"init_sentry",
|
|
18
|
+
"is_sentry_initialized",
|
|
15
19
|
"Settings",
|
|
16
20
|
"settings",
|
|
17
21
|
"env",
|
blaxel/core/common/autoload.py
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import atexit
|
|
2
1
|
import logging
|
|
3
|
-
import sys
|
|
4
|
-
import threading
|
|
5
|
-
from asyncio import CancelledError
|
|
6
|
-
|
|
7
|
-
from sentry_sdk import Client, Hub
|
|
8
2
|
|
|
9
3
|
from ..client import client
|
|
10
4
|
from ..client.response_interceptor import (
|
|
@@ -12,130 +6,11 @@ from ..client.response_interceptor import (
|
|
|
12
6
|
response_interceptors_sync,
|
|
13
7
|
)
|
|
14
8
|
from ..sandbox.client import client as client_sandbox
|
|
9
|
+
from .sentry import init_sentry
|
|
15
10
|
from .settings import settings
|
|
16
11
|
|
|
17
12
|
logger = logging.getLogger(__name__)
|
|
18
13
|
|
|
19
|
-
# Isolated Sentry hub for SDK-only error tracking (doesn't interfere with user's Sentry)
|
|
20
|
-
_sentry_hub: Hub | None = None
|
|
21
|
-
_captured_exceptions: set = set() # Track already captured exceptions to avoid duplicates
|
|
22
|
-
|
|
23
|
-
# Exceptions that are part of normal control flow and should not be captured
|
|
24
|
-
_IGNORED_EXCEPTIONS = (
|
|
25
|
-
StopIteration, # Iterator exhaustion
|
|
26
|
-
StopAsyncIteration, # Async iterator exhaustion
|
|
27
|
-
GeneratorExit, # Generator cleanup
|
|
28
|
-
KeyboardInterrupt, # User interrupt (Ctrl+C)
|
|
29
|
-
SystemExit, # Program exit
|
|
30
|
-
CancelledError, # Async task cancellation
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
# Optional dependencies that may not be installed - import errors for these are expected
|
|
34
|
-
_OPTIONAL_DEPENDENCIES = ("opentelemetry",)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _get_exception_key(exc_type, exc_value, frame) -> str:
|
|
38
|
-
"""Generate a unique key for an exception based on type, message, and origin."""
|
|
39
|
-
# Use type name + message + original file/line where exception was raised
|
|
40
|
-
# This ensures the same logical exception is only captured once
|
|
41
|
-
exc_name = exc_type.__name__ if exc_type else "Unknown"
|
|
42
|
-
exc_msg = str(exc_value) if exc_value else ""
|
|
43
|
-
# Get the original traceback location (where exception was first raised)
|
|
44
|
-
tb = getattr(exc_value, "__traceback__", None)
|
|
45
|
-
if tb:
|
|
46
|
-
# Walk to the deepest frame (origin of exception)
|
|
47
|
-
while tb.tb_next:
|
|
48
|
-
tb = tb.tb_next
|
|
49
|
-
origin = f"{tb.tb_frame.f_code.co_filename}:{tb.tb_lineno}"
|
|
50
|
-
else:
|
|
51
|
-
origin = f"{frame.f_code.co_filename}:{frame.f_lineno}"
|
|
52
|
-
return f"{exc_name}:{exc_msg}:{origin}"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _is_optional_dependency_error(exc_type, exc_value) -> bool:
|
|
56
|
-
"""Check if the exception is an import error for an optional dependency."""
|
|
57
|
-
# ModuleNotFoundError is a subclass of ImportError, so checking ImportError covers both
|
|
58
|
-
if exc_type and issubclass(exc_type, ImportError):
|
|
59
|
-
msg = str(exc_value).lower()
|
|
60
|
-
return any(dep in msg for dep in _OPTIONAL_DEPENDENCIES)
|
|
61
|
-
return False
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _trace_blaxel_exceptions(frame, event, arg):
|
|
65
|
-
"""Trace function that captures exceptions from blaxel SDK code."""
|
|
66
|
-
if event == "exception":
|
|
67
|
-
exc_type, exc_value, exc_tb = arg
|
|
68
|
-
|
|
69
|
-
# Skip control flow exceptions (not actual errors)
|
|
70
|
-
if exc_type and issubclass(exc_type, _IGNORED_EXCEPTIONS):
|
|
71
|
-
return _trace_blaxel_exceptions
|
|
72
|
-
|
|
73
|
-
# Skip import errors for optional dependencies (expected when not installed)
|
|
74
|
-
if _is_optional_dependency_error(exc_type, exc_value):
|
|
75
|
-
return _trace_blaxel_exceptions
|
|
76
|
-
|
|
77
|
-
filename = frame.f_code.co_filename
|
|
78
|
-
|
|
79
|
-
# Only capture if it's from blaxel in site-packages
|
|
80
|
-
if "site-packages/blaxel" in filename:
|
|
81
|
-
# Avoid capturing the same exception multiple times using a content-based key
|
|
82
|
-
exc_key = _get_exception_key(exc_type, exc_value, frame)
|
|
83
|
-
if exc_key not in _captured_exceptions:
|
|
84
|
-
_captured_exceptions.add(exc_key)
|
|
85
|
-
capture_exception(exc_value)
|
|
86
|
-
# Clean up old exception keys to prevent memory leak
|
|
87
|
-
if len(_captured_exceptions) > 1000:
|
|
88
|
-
_captured_exceptions.clear()
|
|
89
|
-
|
|
90
|
-
return _trace_blaxel_exceptions
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def sentry() -> None:
|
|
94
|
-
"""Initialize an isolated Sentry client for SDK error tracking."""
|
|
95
|
-
global _sentry_hub
|
|
96
|
-
try:
|
|
97
|
-
dsn = settings.sentry_dsn
|
|
98
|
-
if not dsn:
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
# Create an isolated client that won't interfere with user's Sentry
|
|
102
|
-
sentry_client = Client(
|
|
103
|
-
dsn=dsn,
|
|
104
|
-
environment=settings.env,
|
|
105
|
-
release=f"sdk-python@{settings.version}",
|
|
106
|
-
default_integrations=False,
|
|
107
|
-
auto_enabling_integrations=False,
|
|
108
|
-
)
|
|
109
|
-
_sentry_hub = Hub(sentry_client)
|
|
110
|
-
|
|
111
|
-
# Set SDK-specific tags
|
|
112
|
-
with _sentry_hub.configure_scope() as scope:
|
|
113
|
-
scope.set_tag("blaxel.workspace", settings.workspace)
|
|
114
|
-
scope.set_tag("blaxel.version", settings.version)
|
|
115
|
-
scope.set_tag("blaxel.commit", settings.commit)
|
|
116
|
-
|
|
117
|
-
# Install trace function to automatically capture SDK exceptions
|
|
118
|
-
sys.settrace(_trace_blaxel_exceptions)
|
|
119
|
-
threading.settrace(_trace_blaxel_exceptions)
|
|
120
|
-
|
|
121
|
-
# Register atexit handler to flush pending events
|
|
122
|
-
atexit.register(_flush_sentry)
|
|
123
|
-
|
|
124
|
-
except Exception as e:
|
|
125
|
-
logger.debug(f"Error initializing Sentry: {e}")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def capture_exception(exception: Exception | None = None) -> None:
|
|
129
|
-
"""Capture an exception to the SDK's isolated Sentry hub."""
|
|
130
|
-
if _sentry_hub is not None and _sentry_hub.client is not None:
|
|
131
|
-
_sentry_hub.capture_exception(exception)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _flush_sentry():
|
|
135
|
-
"""Flush pending Sentry events on program exit."""
|
|
136
|
-
if _sentry_hub is not None and _sentry_hub.client is not None:
|
|
137
|
-
_sentry_hub.client.flush(timeout=2)
|
|
138
|
-
|
|
139
14
|
|
|
140
15
|
def telemetry() -> None:
|
|
141
16
|
from blaxel.telemetry import telemetry_manager
|
|
@@ -164,7 +39,7 @@ def autoload() -> None:
|
|
|
164
39
|
|
|
165
40
|
if settings.tracking:
|
|
166
41
|
try:
|
|
167
|
-
|
|
42
|
+
init_sentry()
|
|
168
43
|
except Exception:
|
|
169
44
|
pass
|
|
170
45
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import traceback
|
|
7
|
+
import uuid
|
|
8
|
+
from asyncio import CancelledError
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .settings import settings
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Lightweight Sentry client using httpx - only captures SDK errors
|
|
20
|
+
_sentry_initialized = False
|
|
21
|
+
_captured_exceptions: set = set() # Track already captured exceptions to avoid duplicates
|
|
22
|
+
|
|
23
|
+
# Parsed DSN components
|
|
24
|
+
_sentry_config: dict[str, str] | None = None
|
|
25
|
+
|
|
26
|
+
# Queue for pending events
|
|
27
|
+
_pending_events: list[dict[str, Any]] = []
|
|
28
|
+
_flush_lock = threading.Lock()
|
|
29
|
+
_handlers_registered = False
|
|
30
|
+
|
|
31
|
+
# Exceptions that are part of normal control flow and should not be captured
|
|
32
|
+
_IGNORED_EXCEPTIONS = (
|
|
33
|
+
StopIteration, # Iterator exhaustion
|
|
34
|
+
StopAsyncIteration, # Async iterator exhaustion
|
|
35
|
+
GeneratorExit, # Generator cleanup
|
|
36
|
+
KeyboardInterrupt, # User interrupt (Ctrl+C)
|
|
37
|
+
SystemExit, # Program exit
|
|
38
|
+
CancelledError, # Async task cancellation
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Optional dependencies that may not be installed - import errors for these are expected
|
|
42
|
+
_OPTIONAL_DEPENDENCIES = ("opentelemetry",)
|
|
43
|
+
|
|
44
|
+
# SDK path patterns to identify errors originating from our SDK
|
|
45
|
+
_SDK_PATTERNS = [
|
|
46
|
+
"blaxel/",
|
|
47
|
+
"blaxel\\",
|
|
48
|
+
"site-packages/blaxel",
|
|
49
|
+
"site-packages\\blaxel",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_from_sdk(error: Exception) -> bool:
|
|
54
|
+
"""Check if an error originated from SDK code based on stack trace."""
|
|
55
|
+
tb = error.__traceback__
|
|
56
|
+
if not tb:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
# Walk through the traceback
|
|
60
|
+
while tb:
|
|
61
|
+
filename = tb.tb_frame.f_code.co_filename
|
|
62
|
+
if any(pattern in filename for pattern in _SDK_PATTERNS):
|
|
63
|
+
return True
|
|
64
|
+
tb = tb.tb_next
|
|
65
|
+
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_dsn(dsn: str) -> dict[str, str] | None:
|
|
70
|
+
"""
|
|
71
|
+
Parse a Sentry DSN into its components.
|
|
72
|
+
DSN format: https://{public_key}@{host}/{project_id}
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
parsed = urlparse(dsn)
|
|
76
|
+
public_key = parsed.username
|
|
77
|
+
host = parsed.hostname
|
|
78
|
+
project_id = parsed.path.lstrip("/")
|
|
79
|
+
|
|
80
|
+
if not public_key or not host or not project_id:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
return {"public_key": public_key, "host": host, "project_id": project_id}
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _generate_event_id() -> str:
|
|
89
|
+
"""Generate a UUID v4 for event ID."""
|
|
90
|
+
return uuid.uuid4().hex
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_stack_trace(exc: Exception) -> list[dict[str, Any]]:
|
|
94
|
+
"""Parse exception traceback into Sentry-compatible frames."""
|
|
95
|
+
frames: list[dict[str, Any]] = []
|
|
96
|
+
tb = traceback.extract_tb(exc.__traceback__)
|
|
97
|
+
|
|
98
|
+
for frame in tb:
|
|
99
|
+
frames.append(
|
|
100
|
+
{
|
|
101
|
+
"filename": frame.filename,
|
|
102
|
+
"function": frame.name or "<anonymous>",
|
|
103
|
+
"lineno": frame.lineno,
|
|
104
|
+
"colno": 0,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return frames
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _error_to_sentry_event(error: Exception) -> dict[str, Any]:
|
|
112
|
+
"""Convert an Exception to a Sentry event payload."""
|
|
113
|
+
frames = _parse_stack_trace(error)
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
"event_id": _generate_event_id(),
|
|
117
|
+
"timestamp": datetime.now(timezone.utc).timestamp(),
|
|
118
|
+
"platform": "python",
|
|
119
|
+
"level": "error",
|
|
120
|
+
"environment": settings.env,
|
|
121
|
+
"release": f"sdk-python@{settings.version}",
|
|
122
|
+
"tags": {
|
|
123
|
+
"blaxel.workspace": settings.workspace,
|
|
124
|
+
"blaxel.version": settings.version,
|
|
125
|
+
"blaxel.commit": settings.commit,
|
|
126
|
+
},
|
|
127
|
+
"exception": {
|
|
128
|
+
"values": [
|
|
129
|
+
{
|
|
130
|
+
"type": type(error).__name__,
|
|
131
|
+
"value": str(error),
|
|
132
|
+
"stacktrace": {"frames": frames},
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _send_to_sentry(event: dict[str, Any]) -> None:
|
|
140
|
+
"""Send an event to Sentry using httpx."""
|
|
141
|
+
if not _sentry_config:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
public_key = _sentry_config["public_key"]
|
|
145
|
+
host = _sentry_config["host"]
|
|
146
|
+
project_id = _sentry_config["project_id"]
|
|
147
|
+
envelope_url = f"https://{host}/api/{project_id}/envelope/"
|
|
148
|
+
|
|
149
|
+
# Create envelope header
|
|
150
|
+
envelope_header = json.dumps(
|
|
151
|
+
{
|
|
152
|
+
"event_id": event["event_id"],
|
|
153
|
+
"sent_at": datetime.now(timezone.utc).isoformat(),
|
|
154
|
+
"dsn": f"https://{public_key}@{host}/{project_id}",
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Create item header
|
|
159
|
+
item_header = json.dumps({"type": "event", "content_type": "application/json"})
|
|
160
|
+
|
|
161
|
+
# Create envelope body
|
|
162
|
+
envelope = f"{envelope_header}\n{item_header}\n{json.dumps(event)}"
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
httpx.post(
|
|
166
|
+
envelope_url,
|
|
167
|
+
headers={
|
|
168
|
+
"Content-Type": "application/x-sentry-envelope",
|
|
169
|
+
"X-Sentry-Auth": f"Sentry sentry_version=7, sentry_client=blaxel-sdk/{settings.version}, sentry_key={public_key}",
|
|
170
|
+
},
|
|
171
|
+
content=envelope,
|
|
172
|
+
timeout=5.0,
|
|
173
|
+
)
|
|
174
|
+
except Exception:
|
|
175
|
+
# Silently fail - error reporting should never break the SDK
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _get_exception_key(exc_type, exc_value, frame) -> str:
|
|
180
|
+
"""Generate a unique key for an exception based on type, message, and origin."""
|
|
181
|
+
exc_name = exc_type.__name__ if exc_type else "Unknown"
|
|
182
|
+
exc_msg = str(exc_value) if exc_value else ""
|
|
183
|
+
tb = getattr(exc_value, "__traceback__", None)
|
|
184
|
+
if tb:
|
|
185
|
+
while tb.tb_next:
|
|
186
|
+
tb = tb.tb_next
|
|
187
|
+
origin = f"{tb.tb_frame.f_code.co_filename}:{tb.tb_lineno}"
|
|
188
|
+
else:
|
|
189
|
+
origin = f"{frame.f_code.co_filename}:{frame.f_lineno}"
|
|
190
|
+
return f"{exc_name}:{exc_msg}:{origin}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _is_optional_dependency_error(exc_type, exc_value) -> bool:
|
|
194
|
+
"""Check if the exception is an import error for an optional dependency."""
|
|
195
|
+
if exc_type and issubclass(exc_type, ImportError):
|
|
196
|
+
msg = str(exc_value).lower()
|
|
197
|
+
return any(dep in msg for dep in _OPTIONAL_DEPENDENCIES)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _trace_blaxel_exceptions(frame, event, arg):
|
|
202
|
+
"""Trace function that captures exceptions from blaxel SDK code."""
|
|
203
|
+
if event == "exception":
|
|
204
|
+
exc_type, exc_value, exc_tb = arg
|
|
205
|
+
|
|
206
|
+
# Skip control flow exceptions (not actual errors)
|
|
207
|
+
if exc_type and issubclass(exc_type, _IGNORED_EXCEPTIONS):
|
|
208
|
+
return _trace_blaxel_exceptions
|
|
209
|
+
|
|
210
|
+
# Skip import errors for optional dependencies (expected when not installed)
|
|
211
|
+
if _is_optional_dependency_error(exc_type, exc_value):
|
|
212
|
+
return _trace_blaxel_exceptions
|
|
213
|
+
|
|
214
|
+
filename = frame.f_code.co_filename
|
|
215
|
+
|
|
216
|
+
# Only capture if it's from blaxel in site-packages
|
|
217
|
+
if "site-packages/blaxel" in filename:
|
|
218
|
+
# Avoid capturing the same exception multiple times using a content-based key
|
|
219
|
+
exc_key = _get_exception_key(exc_type, exc_value, frame)
|
|
220
|
+
if exc_key not in _captured_exceptions:
|
|
221
|
+
_captured_exceptions.add(exc_key)
|
|
222
|
+
capture_exception(exc_value)
|
|
223
|
+
# Clean up old exception keys to prevent memory leak
|
|
224
|
+
if len(_captured_exceptions) > 1000:
|
|
225
|
+
_captured_exceptions.clear()
|
|
226
|
+
|
|
227
|
+
return _trace_blaxel_exceptions
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def init_sentry() -> None:
|
|
231
|
+
"""Initialize the lightweight Sentry client for SDK error tracking."""
|
|
232
|
+
global _sentry_initialized, _sentry_config, _handlers_registered
|
|
233
|
+
try:
|
|
234
|
+
dsn = settings.sentry_dsn
|
|
235
|
+
if not dsn:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
# Parse DSN
|
|
239
|
+
_sentry_config = _parse_dsn(dsn)
|
|
240
|
+
if not _sentry_config:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# Only allow dev/prod environments
|
|
244
|
+
if settings.env not in ("dev", "prod"):
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
_sentry_initialized = True
|
|
248
|
+
|
|
249
|
+
# Register handlers only once
|
|
250
|
+
if not _handlers_registered:
|
|
251
|
+
_handlers_registered = True
|
|
252
|
+
|
|
253
|
+
# Install trace function to automatically capture SDK exceptions
|
|
254
|
+
sys.settrace(_trace_blaxel_exceptions)
|
|
255
|
+
threading.settrace(_trace_blaxel_exceptions)
|
|
256
|
+
|
|
257
|
+
# Register atexit handler to flush pending events
|
|
258
|
+
atexit.register(flush_sentry)
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.debug(f"Error initializing Sentry: {e}")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def capture_exception(exception: Exception | None = None) -> None:
|
|
265
|
+
"""Capture an exception to Sentry.
|
|
266
|
+
Only errors originating from SDK code will be captured.
|
|
267
|
+
"""
|
|
268
|
+
if not _sentry_initialized or not _sentry_config or exception is None:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
# Generate unique key to prevent duplicate captures
|
|
273
|
+
exc_key = f"{type(exception).__name__}:{str(exception)}"
|
|
274
|
+
if exc_key in _captured_exceptions:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
_captured_exceptions.add(exc_key)
|
|
278
|
+
|
|
279
|
+
# Clean up old exception keys to prevent memory leak
|
|
280
|
+
if len(_captured_exceptions) > 1000:
|
|
281
|
+
_captured_exceptions.clear()
|
|
282
|
+
|
|
283
|
+
# Convert error to Sentry event and queue it
|
|
284
|
+
event = _error_to_sentry_event(exception)
|
|
285
|
+
with _flush_lock:
|
|
286
|
+
_pending_events.append(event)
|
|
287
|
+
|
|
288
|
+
# Send immediately (fire and forget)
|
|
289
|
+
_send_to_sentry(event)
|
|
290
|
+
|
|
291
|
+
except Exception:
|
|
292
|
+
# Silently fail - error capturing should never break the SDK
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def flush_sentry(timeout: float = 2.0) -> None:
|
|
297
|
+
"""Flush pending Sentry events."""
|
|
298
|
+
if not _sentry_initialized:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
with _flush_lock:
|
|
302
|
+
if not _pending_events:
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
events_to_send = _pending_events.copy()
|
|
306
|
+
_pending_events.clear()
|
|
307
|
+
|
|
308
|
+
# Send all pending events
|
|
309
|
+
for event in events_to_send:
|
|
310
|
+
try:
|
|
311
|
+
_send_to_sentry(event)
|
|
312
|
+
except Exception:
|
|
313
|
+
# Silently fail
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def is_sentry_initialized() -> bool:
|
|
318
|
+
"""Check if Sentry is initialized and available."""
|
|
319
|
+
return _sentry_initialized
|