sentry-sdk 3.0.0a4__py2.py3-none-any.whl → 3.0.0a6__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.
Potentially problematic release.
This version of sentry-sdk might be problematic. Click here for more details.
- sentry_sdk/__init__.py +1 -0
- sentry_sdk/ai/utils.py +7 -8
- sentry_sdk/api.py +68 -0
- sentry_sdk/client.py +93 -17
- sentry_sdk/consts.py +126 -9
- sentry_sdk/crons/api.py +5 -0
- sentry_sdk/integrations/anthropic.py +133 -73
- sentry_sdk/integrations/asgi.py +10 -9
- sentry_sdk/integrations/asyncio.py +85 -20
- sentry_sdk/integrations/clickhouse_driver.py +55 -28
- sentry_sdk/integrations/fastapi.py +1 -7
- sentry_sdk/integrations/gnu_backtrace.py +6 -3
- sentry_sdk/integrations/langchain.py +462 -218
- sentry_sdk/integrations/litestar.py +1 -1
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +0 -2
- sentry_sdk/integrations/openai_agents/patches/runner.py +18 -15
- sentry_sdk/integrations/quart.py +1 -1
- sentry_sdk/integrations/starlette.py +1 -5
- sentry_sdk/integrations/starlite.py +1 -1
- sentry_sdk/opentelemetry/scope.py +3 -1
- sentry_sdk/opentelemetry/span_processor.py +1 -0
- sentry_sdk/scope.py +11 -11
- sentry_sdk/tracing.py +100 -18
- sentry_sdk/tracing_utils.py +330 -33
- sentry_sdk/transport.py +357 -62
- sentry_sdk/utils.py +23 -5
- sentry_sdk/worker.py +197 -3
- {sentry_sdk-3.0.0a4.dist-info → sentry_sdk-3.0.0a6.dist-info}/METADATA +3 -1
- {sentry_sdk-3.0.0a4.dist-info → sentry_sdk-3.0.0a6.dist-info}/RECORD +33 -33
- {sentry_sdk-3.0.0a4.dist-info → sentry_sdk-3.0.0a6.dist-info}/WHEEL +0 -0
- {sentry_sdk-3.0.0a4.dist-info → sentry_sdk-3.0.0a6.dist-info}/entry_points.txt +0 -0
- {sentry_sdk-3.0.0a4.dist-info → sentry_sdk-3.0.0a6.dist-info}/licenses/LICENSE +0 -0
- {sentry_sdk-3.0.0a4.dist-info → sentry_sdk-3.0.0a6.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from functools import wraps
|
|
3
|
+
import json
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
import sentry_sdk
|
|
6
7
|
from sentry_sdk.ai.monitoring import record_token_usage
|
|
8
|
+
from sentry_sdk.ai.utils import set_data_normalized
|
|
7
9
|
from sentry_sdk.consts import OP, SPANDATA
|
|
8
10
|
from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
|
|
9
11
|
from sentry_sdk.scope import should_send_default_pii
|
|
@@ -11,9 +13,15 @@ from sentry_sdk.utils import (
|
|
|
11
13
|
capture_internal_exceptions,
|
|
12
14
|
event_from_exception,
|
|
13
15
|
package_version,
|
|
16
|
+
safe_serialize,
|
|
14
17
|
)
|
|
15
18
|
|
|
16
19
|
try:
|
|
20
|
+
try:
|
|
21
|
+
from anthropic import NOT_GIVEN
|
|
22
|
+
except ImportError:
|
|
23
|
+
NOT_GIVEN = None
|
|
24
|
+
|
|
17
25
|
from anthropic.resources import AsyncMessages, Messages
|
|
18
26
|
|
|
19
27
|
if TYPE_CHECKING:
|
|
@@ -51,7 +59,10 @@ def _capture_exception(exc: Any) -> None:
|
|
|
51
59
|
sentry_sdk.capture_event(event, hint=hint)
|
|
52
60
|
|
|
53
61
|
|
|
54
|
-
def
|
|
62
|
+
def _get_token_usage(result: Messages) -> tuple[int, int]:
|
|
63
|
+
"""
|
|
64
|
+
Get token usage from the Anthropic response.
|
|
65
|
+
"""
|
|
55
66
|
input_tokens = 0
|
|
56
67
|
output_tokens = 0
|
|
57
68
|
if hasattr(result, "usage"):
|
|
@@ -61,40 +72,18 @@ def _calculate_token_usage(result: Messages, span: Span) -> None:
|
|
|
61
72
|
if hasattr(usage, "output_tokens") and isinstance(usage.output_tokens, int):
|
|
62
73
|
output_tokens = usage.output_tokens
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
record_token_usage(
|
|
67
|
-
span,
|
|
68
|
-
input_tokens=input_tokens,
|
|
69
|
-
output_tokens=output_tokens,
|
|
70
|
-
total_tokens=total_tokens,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _get_responses(content: list[Any]) -> list[dict[str, Any]]:
|
|
75
|
-
"""
|
|
76
|
-
Get JSON of a Anthropic responses.
|
|
77
|
-
"""
|
|
78
|
-
responses = []
|
|
79
|
-
for item in content:
|
|
80
|
-
if hasattr(item, "text"):
|
|
81
|
-
responses.append(
|
|
82
|
-
{
|
|
83
|
-
"type": item.type,
|
|
84
|
-
"text": item.text,
|
|
85
|
-
}
|
|
86
|
-
)
|
|
87
|
-
return responses
|
|
75
|
+
return input_tokens, output_tokens
|
|
88
76
|
|
|
89
77
|
|
|
90
78
|
def _collect_ai_data(
|
|
91
79
|
event: MessageStreamEvent,
|
|
80
|
+
model: str | None,
|
|
92
81
|
input_tokens: int,
|
|
93
82
|
output_tokens: int,
|
|
94
83
|
content_blocks: list[str],
|
|
95
|
-
) -> tuple[int, int, list[str]]:
|
|
84
|
+
) -> tuple[str | None, int, int, list[str]]:
|
|
96
85
|
"""
|
|
97
|
-
|
|
86
|
+
Collect model information, token usage, and collect content blocks from the AI streaming response.
|
|
98
87
|
"""
|
|
99
88
|
with capture_internal_exceptions():
|
|
100
89
|
if hasattr(event, "type"):
|
|
@@ -102,6 +91,7 @@ def _collect_ai_data(
|
|
|
102
91
|
usage = event.message.usage
|
|
103
92
|
input_tokens += usage.input_tokens
|
|
104
93
|
output_tokens += usage.output_tokens
|
|
94
|
+
model = event.message.model or model
|
|
105
95
|
elif event.type == "content_block_start":
|
|
106
96
|
pass
|
|
107
97
|
elif event.type == "content_block_delta":
|
|
@@ -114,34 +104,80 @@ def _collect_ai_data(
|
|
|
114
104
|
elif event.type == "message_delta":
|
|
115
105
|
output_tokens += event.usage.output_tokens
|
|
116
106
|
|
|
117
|
-
return input_tokens, output_tokens, content_blocks
|
|
107
|
+
return model, input_tokens, output_tokens, content_blocks
|
|
118
108
|
|
|
119
109
|
|
|
120
|
-
def
|
|
110
|
+
def _set_input_data(
|
|
111
|
+
span: Span, kwargs: dict[str, Any], integration: AnthropicIntegration
|
|
112
|
+
) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Set input data for the span based on the provided keyword arguments for the anthropic message creation.
|
|
115
|
+
"""
|
|
116
|
+
messages = kwargs.get("messages")
|
|
117
|
+
if (
|
|
118
|
+
messages is not None
|
|
119
|
+
and len(messages) > 0
|
|
120
|
+
and should_send_default_pii()
|
|
121
|
+
and integration.include_prompts
|
|
122
|
+
):
|
|
123
|
+
set_data_normalized(
|
|
124
|
+
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, safe_serialize(messages)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
set_data_normalized(
|
|
128
|
+
span, SPANDATA.GEN_AI_RESPONSE_STREAMING, kwargs.get("stream", False)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
kwargs_keys_to_attributes = {
|
|
132
|
+
"max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS,
|
|
133
|
+
"model": SPANDATA.GEN_AI_REQUEST_MODEL,
|
|
134
|
+
"temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE,
|
|
135
|
+
"top_k": SPANDATA.GEN_AI_REQUEST_TOP_K,
|
|
136
|
+
"top_p": SPANDATA.GEN_AI_REQUEST_TOP_P,
|
|
137
|
+
}
|
|
138
|
+
for key, attribute in kwargs_keys_to_attributes.items():
|
|
139
|
+
value = kwargs.get(key)
|
|
140
|
+
if value is not NOT_GIVEN and value is not None:
|
|
141
|
+
set_data_normalized(span, attribute, value)
|
|
142
|
+
|
|
143
|
+
# Input attributes: Tools
|
|
144
|
+
tools = kwargs.get("tools")
|
|
145
|
+
if tools is not NOT_GIVEN and tools is not None and len(tools) > 0:
|
|
146
|
+
set_data_normalized(
|
|
147
|
+
span, SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _set_output_data(
|
|
121
152
|
span: Span,
|
|
122
153
|
integration: AnthropicIntegration,
|
|
154
|
+
model: str | None,
|
|
123
155
|
input_tokens: int,
|
|
124
156
|
output_tokens: int,
|
|
125
|
-
content_blocks: list[
|
|
157
|
+
content_blocks: list[Any],
|
|
158
|
+
finish_span: bool = False,
|
|
126
159
|
) -> None:
|
|
127
160
|
"""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
complete_message = "".join(content_blocks)
|
|
133
|
-
span.set_attribute(
|
|
134
|
-
SPANDATA.AI_RESPONSES,
|
|
135
|
-
[{"type": "text", "text": complete_message}],
|
|
136
|
-
)
|
|
137
|
-
total_tokens = input_tokens + output_tokens
|
|
138
|
-
record_token_usage(
|
|
161
|
+
Set output data for the span based on the AI response."""
|
|
162
|
+
span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, model)
|
|
163
|
+
if should_send_default_pii() and integration.include_prompts:
|
|
164
|
+
set_data_normalized(
|
|
139
165
|
span,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
SPANDATA.GEN_AI_RESPONSE_TEXT,
|
|
167
|
+
json.dumps(content_blocks),
|
|
168
|
+
unpack=False,
|
|
143
169
|
)
|
|
144
|
-
|
|
170
|
+
|
|
171
|
+
record_token_usage(
|
|
172
|
+
span,
|
|
173
|
+
input_tokens=input_tokens,
|
|
174
|
+
output_tokens=output_tokens,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# TODO: GEN_AI_RESPONSE_TOOL_CALLS ?
|
|
178
|
+
|
|
179
|
+
if finish_span:
|
|
180
|
+
span.__exit__(None, None, None)
|
|
145
181
|
|
|
146
182
|
|
|
147
183
|
def _sentry_patched_create_common(f: Any, *args: Any, **kwargs: Any) -> Any:
|
|
@@ -157,70 +193,94 @@ def _sentry_patched_create_common(f: Any, *args: Any, **kwargs: Any) -> Any:
|
|
|
157
193
|
except TypeError:
|
|
158
194
|
return f(*args, **kwargs)
|
|
159
195
|
|
|
196
|
+
model = kwargs.get("model", "")
|
|
197
|
+
|
|
160
198
|
span = sentry_sdk.start_span(
|
|
161
|
-
op=OP.
|
|
162
|
-
|
|
199
|
+
op=OP.GEN_AI_CHAT,
|
|
200
|
+
name=f"chat {model}".strip(),
|
|
163
201
|
origin=AnthropicIntegration.origin,
|
|
164
202
|
only_as_child_span=True,
|
|
165
203
|
)
|
|
166
204
|
span.__enter__()
|
|
167
205
|
|
|
168
|
-
|
|
206
|
+
_set_input_data(span, kwargs, integration)
|
|
169
207
|
|
|
170
|
-
|
|
171
|
-
messages = list(kwargs["messages"])
|
|
172
|
-
model = kwargs.get("model")
|
|
208
|
+
result = yield f, args, kwargs
|
|
173
209
|
|
|
174
210
|
with capture_internal_exceptions():
|
|
175
|
-
span.set_attribute(SPANDATA.AI_MODEL_ID, model)
|
|
176
|
-
span.set_attribute(SPANDATA.AI_STREAMING, False)
|
|
177
|
-
|
|
178
|
-
if should_send_default_pii() and integration.include_prompts:
|
|
179
|
-
span.set_attribute(SPANDATA.AI_INPUT_MESSAGES, messages)
|
|
180
|
-
|
|
181
211
|
if hasattr(result, "content"):
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
input_tokens, output_tokens = _get_token_usage(result)
|
|
213
|
+
|
|
214
|
+
content_blocks = []
|
|
215
|
+
for content_block in result.content:
|
|
216
|
+
if hasattr(content_block, "to_dict"):
|
|
217
|
+
content_blocks.append(content_block.to_dict())
|
|
218
|
+
elif hasattr(content_block, "model_dump"):
|
|
219
|
+
content_blocks.append(content_block.model_dump())
|
|
220
|
+
elif hasattr(content_block, "text"):
|
|
221
|
+
content_blocks.append({"type": "text", "text": content_block.text})
|
|
222
|
+
|
|
223
|
+
_set_output_data(
|
|
224
|
+
span=span,
|
|
225
|
+
integration=integration,
|
|
226
|
+
model=getattr(result, "model", None),
|
|
227
|
+
input_tokens=input_tokens,
|
|
228
|
+
output_tokens=output_tokens,
|
|
229
|
+
content_blocks=content_blocks,
|
|
230
|
+
finish_span=True,
|
|
231
|
+
)
|
|
188
232
|
|
|
189
233
|
# Streaming response
|
|
190
234
|
elif hasattr(result, "_iterator"):
|
|
191
235
|
old_iterator = result._iterator
|
|
192
236
|
|
|
193
237
|
def new_iterator() -> Iterator[MessageStreamEvent]:
|
|
238
|
+
model = None
|
|
194
239
|
input_tokens = 0
|
|
195
240
|
output_tokens = 0
|
|
196
241
|
content_blocks: list[str] = []
|
|
197
242
|
|
|
198
243
|
for event in old_iterator:
|
|
199
|
-
input_tokens, output_tokens, content_blocks =
|
|
200
|
-
|
|
244
|
+
model, input_tokens, output_tokens, content_blocks = (
|
|
245
|
+
_collect_ai_data(
|
|
246
|
+
event, model, input_tokens, output_tokens, content_blocks
|
|
247
|
+
)
|
|
201
248
|
)
|
|
202
249
|
yield event
|
|
203
250
|
|
|
204
|
-
|
|
205
|
-
span,
|
|
251
|
+
_set_output_data(
|
|
252
|
+
span=span,
|
|
253
|
+
integration=integration,
|
|
254
|
+
model=model,
|
|
255
|
+
input_tokens=input_tokens,
|
|
256
|
+
output_tokens=output_tokens,
|
|
257
|
+
content_blocks=[{"text": "".join(content_blocks), "type": "text"}],
|
|
258
|
+
finish_span=True,
|
|
206
259
|
)
|
|
207
|
-
span.__exit__(None, None, None)
|
|
208
260
|
|
|
209
261
|
async def new_iterator_async() -> AsyncIterator[MessageStreamEvent]:
|
|
262
|
+
model = None
|
|
210
263
|
input_tokens = 0
|
|
211
264
|
output_tokens = 0
|
|
212
265
|
content_blocks: list[str] = []
|
|
213
266
|
|
|
214
267
|
async for event in old_iterator:
|
|
215
|
-
input_tokens, output_tokens, content_blocks =
|
|
216
|
-
|
|
268
|
+
model, input_tokens, output_tokens, content_blocks = (
|
|
269
|
+
_collect_ai_data(
|
|
270
|
+
event, model, input_tokens, output_tokens, content_blocks
|
|
271
|
+
)
|
|
217
272
|
)
|
|
218
273
|
yield event
|
|
219
274
|
|
|
220
|
-
|
|
221
|
-
span,
|
|
275
|
+
_set_output_data(
|
|
276
|
+
span=span,
|
|
277
|
+
integration=integration,
|
|
278
|
+
model=model,
|
|
279
|
+
input_tokens=input_tokens,
|
|
280
|
+
output_tokens=output_tokens,
|
|
281
|
+
content_blocks=[{"text": "".join(content_blocks), "type": "text"}],
|
|
282
|
+
finish_span=True,
|
|
222
283
|
)
|
|
223
|
-
span.__exit__(None, None, None)
|
|
224
284
|
|
|
225
285
|
if str(type(result._iterator)) == "<class 'async_generator'>":
|
|
226
286
|
result._iterator = new_iterator_async()
|
sentry_sdk/integrations/asgi.py
CHANGED
|
@@ -105,6 +105,7 @@ class SentryAsgiMiddleware:
|
|
|
105
105
|
mechanism_type: str = "asgi",
|
|
106
106
|
span_origin: Optional[str] = None,
|
|
107
107
|
http_methods_to_capture: Tuple[str, ...] = DEFAULT_HTTP_METHODS_TO_CAPTURE,
|
|
108
|
+
asgi_version: Optional[int] = None,
|
|
108
109
|
) -> None:
|
|
109
110
|
"""
|
|
110
111
|
Instrument an ASGI application with Sentry. Provides HTTP/websocket
|
|
@@ -142,10 +143,16 @@ class SentryAsgiMiddleware:
|
|
|
142
143
|
self.app = app
|
|
143
144
|
self.http_methods_to_capture = http_methods_to_capture
|
|
144
145
|
|
|
145
|
-
if
|
|
146
|
+
if asgi_version is None:
|
|
147
|
+
if _looks_like_asgi3(app):
|
|
148
|
+
asgi_version = 3
|
|
149
|
+
else:
|
|
150
|
+
asgi_version = 2
|
|
151
|
+
|
|
152
|
+
if asgi_version == 3:
|
|
146
153
|
self.__call__: Callable[..., Any] = self._run_asgi3
|
|
147
|
-
|
|
148
|
-
self.__call__ = self._run_asgi2
|
|
154
|
+
elif asgi_version == 2:
|
|
155
|
+
self.__call__: Callable[..., Any] = self._run_asgi2 # type: ignore
|
|
149
156
|
|
|
150
157
|
def _capture_lifespan_exception(self, exc: Exception) -> None:
|
|
151
158
|
"""Capture exceptions raise in application lifespan handlers.
|
|
@@ -299,12 +306,6 @@ class SentryAsgiMiddleware:
|
|
|
299
306
|
event["transaction"] = name
|
|
300
307
|
event["transaction_info"] = {"source": source}
|
|
301
308
|
|
|
302
|
-
logger.debug(
|
|
303
|
-
"[ASGI] Set transaction name and source in event_processor: '%s' / '%s'",
|
|
304
|
-
event["transaction"],
|
|
305
|
-
event["transaction_info"]["source"],
|
|
306
|
-
)
|
|
307
|
-
|
|
308
309
|
return event
|
|
309
310
|
|
|
310
311
|
# Helper functions.
|
|
@@ -4,7 +4,13 @@ import sys
|
|
|
4
4
|
import sentry_sdk
|
|
5
5
|
from sentry_sdk.consts import OP
|
|
6
6
|
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
7
|
-
from sentry_sdk.utils import
|
|
7
|
+
from sentry_sdk.utils import (
|
|
8
|
+
event_from_exception,
|
|
9
|
+
logger,
|
|
10
|
+
reraise,
|
|
11
|
+
is_internal_task,
|
|
12
|
+
)
|
|
13
|
+
from sentry_sdk.transport import AsyncHttpTransport
|
|
8
14
|
|
|
9
15
|
try:
|
|
10
16
|
import asyncio
|
|
@@ -29,6 +35,72 @@ def get_name(coro: Any) -> str:
|
|
|
29
35
|
)
|
|
30
36
|
|
|
31
37
|
|
|
38
|
+
def patch_loop_close() -> None:
|
|
39
|
+
"""Patch loop.close to flush pending events before shutdown."""
|
|
40
|
+
# Atexit shutdown hook happens after the event loop is closed.
|
|
41
|
+
# Therefore, it is necessary to patch the loop.close method to ensure
|
|
42
|
+
# that pending events are flushed before the interpreter shuts down.
|
|
43
|
+
try:
|
|
44
|
+
loop = asyncio.get_running_loop()
|
|
45
|
+
except RuntimeError:
|
|
46
|
+
# No running loop → cannot patch now
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if getattr(loop, "_sentry_flush_patched", False):
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
async def _flush() -> None:
|
|
53
|
+
client = sentry_sdk.get_client()
|
|
54
|
+
if not client:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
if not isinstance(client.transport, AsyncHttpTransport):
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
await client.close_async()
|
|
62
|
+
except Exception:
|
|
63
|
+
logger.warning("Sentry flush failed during loop shutdown", exc_info=True)
|
|
64
|
+
|
|
65
|
+
orig_close = loop.close
|
|
66
|
+
|
|
67
|
+
def _patched_close() -> None:
|
|
68
|
+
try:
|
|
69
|
+
loop.run_until_complete(_flush())
|
|
70
|
+
finally:
|
|
71
|
+
orig_close()
|
|
72
|
+
|
|
73
|
+
loop.close = _patched_close # type: ignore
|
|
74
|
+
loop._sentry_flush_patched = True # type: ignore
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _create_task_with_factory(
|
|
78
|
+
orig_task_factory: Any,
|
|
79
|
+
loop: asyncio.AbstractEventLoop,
|
|
80
|
+
coro: Coroutine[Any, Any, Any],
|
|
81
|
+
**kwargs: Any,
|
|
82
|
+
) -> asyncio.Task[Any]:
|
|
83
|
+
task = None
|
|
84
|
+
|
|
85
|
+
# Trying to use user set task factory (if there is one)
|
|
86
|
+
if orig_task_factory:
|
|
87
|
+
task = orig_task_factory(loop, coro, **kwargs)
|
|
88
|
+
|
|
89
|
+
if task is None:
|
|
90
|
+
# The default task factory in `asyncio` does not have its own function
|
|
91
|
+
# but is just a couple of lines in `asyncio.base_events.create_task()`
|
|
92
|
+
# Those lines are copied here.
|
|
93
|
+
|
|
94
|
+
# WARNING:
|
|
95
|
+
# If the default behavior of the task creation in asyncio changes,
|
|
96
|
+
# this will break!
|
|
97
|
+
task = Task(coro, loop=loop, **kwargs)
|
|
98
|
+
if task._source_traceback: # type: ignore
|
|
99
|
+
del task._source_traceback[-1] # type: ignore
|
|
100
|
+
|
|
101
|
+
return task
|
|
102
|
+
|
|
103
|
+
|
|
32
104
|
def patch_asyncio() -> None:
|
|
33
105
|
orig_task_factory = None
|
|
34
106
|
try:
|
|
@@ -41,6 +113,14 @@ def patch_asyncio() -> None:
|
|
|
41
113
|
**kwargs: Any,
|
|
42
114
|
) -> asyncio.Future[Any]:
|
|
43
115
|
|
|
116
|
+
# Check if this is an internal Sentry task
|
|
117
|
+
is_internal = is_internal_task()
|
|
118
|
+
|
|
119
|
+
if is_internal:
|
|
120
|
+
return _create_task_with_factory(
|
|
121
|
+
orig_task_factory, loop, coro, **kwargs
|
|
122
|
+
)
|
|
123
|
+
|
|
44
124
|
async def _task_with_sentry_span_creation() -> Any:
|
|
45
125
|
result = None
|
|
46
126
|
|
|
@@ -58,25 +138,9 @@ def patch_asyncio() -> None:
|
|
|
58
138
|
|
|
59
139
|
return result
|
|
60
140
|
|
|
61
|
-
task =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if orig_task_factory:
|
|
65
|
-
task = orig_task_factory(
|
|
66
|
-
loop, _task_with_sentry_span_creation(), **kwargs
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
if task is None:
|
|
70
|
-
# The default task factory in `asyncio` does not have its own function
|
|
71
|
-
# but is just a couple of lines in `asyncio.base_events.create_task()`
|
|
72
|
-
# Those lines are copied here.
|
|
73
|
-
|
|
74
|
-
# WARNING:
|
|
75
|
-
# If the default behavior of the task creation in asyncio changes,
|
|
76
|
-
# this will break!
|
|
77
|
-
task = Task(_task_with_sentry_span_creation(), loop=loop, **kwargs)
|
|
78
|
-
if task._source_traceback: # type: ignore
|
|
79
|
-
del task._source_traceback[-1] # type: ignore
|
|
141
|
+
task = _create_task_with_factory(
|
|
142
|
+
orig_task_factory, loop, _task_with_sentry_span_creation(), **kwargs
|
|
143
|
+
)
|
|
80
144
|
|
|
81
145
|
# Set the task name to include the original coroutine's name
|
|
82
146
|
try:
|
|
@@ -124,3 +188,4 @@ class AsyncioIntegration(Integration):
|
|
|
124
188
|
@staticmethod
|
|
125
189
|
def setup_once() -> None:
|
|
126
190
|
patch_asyncio()
|
|
191
|
+
patch_loop_close()
|
|
@@ -13,7 +13,8 @@ from sentry_sdk.utils import (
|
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
|
-
from
|
|
16
|
+
from collections.abc import Iterator
|
|
17
|
+
from typing import Any, ParamSpec, Callable, TypeVar
|
|
17
18
|
|
|
18
19
|
P = ParamSpec("P")
|
|
19
20
|
T = TypeVar("T")
|
|
@@ -40,9 +41,7 @@ class ClickhouseDriverIntegration(Integration):
|
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
# If the query contains parameters then the send_data function is used to send those parameters to clickhouse
|
|
43
|
-
|
|
44
|
-
clickhouse_driver.client.Client.send_data
|
|
45
|
-
)
|
|
44
|
+
_wrap_send_data()
|
|
46
45
|
|
|
47
46
|
# Every query ends either with the Client's `receive_end_of_query` (no result expected)
|
|
48
47
|
# or its `receive_result` (result expected)
|
|
@@ -134,36 +133,64 @@ def _wrap_end(f: Callable[P, T]) -> Callable[P, T]:
|
|
|
134
133
|
return _inner_end
|
|
135
134
|
|
|
136
135
|
|
|
137
|
-
def _wrap_send_data(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
def _wrap_send_data() -> None:
|
|
137
|
+
original_send_data = clickhouse_driver.client.Client.send_data
|
|
138
|
+
|
|
139
|
+
def _inner_send_data(
|
|
140
|
+
self: clickhouse_driver.client.Client,
|
|
141
|
+
sample_block: Any,
|
|
142
|
+
data: Any,
|
|
143
|
+
types_check: bool = False,
|
|
144
|
+
columnar: bool = False,
|
|
145
|
+
*args: Any,
|
|
146
|
+
**kwargs: Any,
|
|
147
|
+
) -> Any:
|
|
148
|
+
span = getattr(self.connection, "_sentry_span", None)
|
|
149
|
+
if span is None:
|
|
150
|
+
return original_send_data(
|
|
151
|
+
self, sample_block, data, types_check, columnar, *args, **kwargs
|
|
152
|
+
)
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
span
|
|
154
|
+
db_data = _get_db_data(self.connection)
|
|
155
|
+
_set_on_span(span, db_data)
|
|
145
156
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
saved_db_data: dict[str, Any] = getattr(self.connection, "_sentry_db_data", {})
|
|
158
|
+
db_params: list[Any] = saved_db_data.get("db.params") or []
|
|
159
|
+
|
|
160
|
+
if should_send_default_pii():
|
|
161
|
+
if isinstance(data, (list, tuple)):
|
|
162
|
+
db_params.extend(data)
|
|
163
|
+
|
|
164
|
+
else: # data is a generic iterator
|
|
165
|
+
orig_data = data
|
|
166
|
+
|
|
167
|
+
# Wrap the generator to add items to db.params as they are yielded.
|
|
168
|
+
# This allows us to send the params to Sentry without needing to allocate
|
|
169
|
+
# memory for the entire generator at once.
|
|
170
|
+
def wrapped_generator() -> "Iterator[Any]":
|
|
171
|
+
for item in orig_data:
|
|
172
|
+
db_params.append(item)
|
|
173
|
+
yield item
|
|
174
|
+
|
|
175
|
+
# Replace the original iterator with the wrapped one.
|
|
176
|
+
data = wrapped_generator()
|
|
177
|
+
|
|
178
|
+
rv = original_send_data(
|
|
179
|
+
self, sample_block, data, types_check, columnar, *args, **kwargs
|
|
180
|
+
)
|
|
149
181
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
db_params_data = args[2]
|
|
156
|
-
if isinstance(db_params_data, list):
|
|
157
|
-
db_params.extend(db_params_data)
|
|
158
|
-
saved_db_data["db.params"] = db_params
|
|
159
|
-
span.set_attribute("db.params", _serialize_span_attribute(db_params))
|
|
182
|
+
if should_send_default_pii() and db_params:
|
|
183
|
+
# need to do this after the original function call to make sure
|
|
184
|
+
# db_params is populated correctly
|
|
185
|
+
saved_db_data["db.params"] = db_params
|
|
186
|
+
span.set_attribute("db.params", _serialize_span_attribute(db_params))
|
|
160
187
|
|
|
161
|
-
return
|
|
188
|
+
return rv
|
|
162
189
|
|
|
163
|
-
|
|
190
|
+
clickhouse_driver.client.Client.send_data = _inner_send_data
|
|
164
191
|
|
|
165
192
|
|
|
166
|
-
def _get_db_data(connection: clickhouse_driver.connection.Connection) ->
|
|
193
|
+
def _get_db_data(connection: clickhouse_driver.connection.Connection) -> dict[str, str]:
|
|
167
194
|
return {
|
|
168
195
|
SPANDATA.DB_SYSTEM: "clickhouse",
|
|
169
196
|
SPANDATA.SERVER_ADDRESS: connection.host,
|
|
@@ -173,6 +200,6 @@ def _get_db_data(connection: clickhouse_driver.connection.Connection) -> Dict[st
|
|
|
173
200
|
}
|
|
174
201
|
|
|
175
202
|
|
|
176
|
-
def _set_on_span(span: Span, data:
|
|
203
|
+
def _set_on_span(span: Span, data: dict[str, Any]) -> None:
|
|
177
204
|
for key, value in data.items():
|
|
178
205
|
span.set_attribute(key, _serialize_span_attribute(value))
|
|
@@ -7,10 +7,7 @@ import sentry_sdk
|
|
|
7
7
|
from sentry_sdk.consts import SOURCE_FOR_STYLE, TransactionSource
|
|
8
8
|
from sentry_sdk.integrations import DidNotEnable
|
|
9
9
|
from sentry_sdk.scope import should_send_default_pii
|
|
10
|
-
from sentry_sdk.utils import
|
|
11
|
-
transaction_from_function,
|
|
12
|
-
logger,
|
|
13
|
-
)
|
|
10
|
+
from sentry_sdk.utils import transaction_from_function
|
|
14
11
|
|
|
15
12
|
from typing import TYPE_CHECKING
|
|
16
13
|
|
|
@@ -67,9 +64,6 @@ def _set_transaction_name_and_source(
|
|
|
67
64
|
source = SOURCE_FOR_STYLE[transaction_style]
|
|
68
65
|
|
|
69
66
|
scope.set_transaction_name(name, source=source)
|
|
70
|
-
logger.debug(
|
|
71
|
-
"[FastAPI] Set transaction name and source on scope: %s / %s", name, source
|
|
72
|
-
)
|
|
73
67
|
|
|
74
68
|
|
|
75
69
|
def patch_get_request_handler() -> None:
|
|
@@ -12,13 +12,16 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
from sentry_sdk._types import Event
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# function is everything between index at @
|
|
16
|
+
# and then we match on the @ plus the hex val
|
|
17
|
+
FUNCTION_RE = r"[^@]+?"
|
|
18
|
+
HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+"
|
|
17
19
|
|
|
18
20
|
FRAME_RE = r"""
|
|
19
|
-
^(?P<index>\d+)\.\s+(?P<function>{FUNCTION_RE}
|
|
21
|
+
^(?P<index>\d+)\.\s+(?P<function>{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P<package>.+))?$
|
|
20
22
|
""".format(
|
|
21
23
|
FUNCTION_RE=FUNCTION_RE,
|
|
24
|
+
HEX_ADDRESS=HEX_ADDRESS,
|
|
22
25
|
)
|
|
23
26
|
|
|
24
27
|
FRAME_RE = re.compile(FRAME_RE, re.MULTILINE | re.VERBOSE)
|