Skip to content

Commit 20e46fc

Browse files
Merge branch 'master' into constantinius/fix/integrations/pydantic-ai-report-image-inputs
2 parents a488747 + 121c9d2 commit 20e46fc

File tree

15 files changed

+354
-139
lines changed

15 files changed

+354
-139
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
token: ${{ steps.token.outputs.token }}
3232
fetch-depth: 0
3333
- name: Prepare release
34-
uses: getsentry/craft@39ee616a6a58dc64797feecb145d66770492b66c # v2
34+
uses: getsentry/craft@1c58bfd57bfd6a967b6f3fc92bead2c42ee698ce # v2
3535
env:
3636
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
3737
with:

scripts/populate_tox/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,9 @@
311311
"deps": {
312312
"*": ["mockupdb"],
313313
},
314+
"python": {
315+
"<3.6": "<3.7",
316+
},
314317
},
315318
"pyramid": {
316319
"package": "pyramid",
@@ -368,6 +371,9 @@
368371
">=0.9,<0.14": ["fakeredis>=1.0,<1.7.4"],
369372
"py3.6,py3.7": ["fakeredis!=2.26.0"],
370373
},
374+
"python": {
375+
"<0.13": "<3.7",
376+
},
371377
},
372378
"sanic": {
373379
"package": "sanic",
@@ -385,6 +391,9 @@
385391
},
386392
"sqlalchemy": {
387393
"package": "sqlalchemy",
394+
"python": {
395+
"<1.4": "<3.10",
396+
},
388397
},
389398
"starlette": {
390399
"package": "starlette",

scripts/populate_tox/package_dependencies.jsonl

Lines changed: 15 additions & 13 deletions
Large diffs are not rendered by default.

scripts/populate_tox/populate_tox.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,9 @@ def pick_python_versions_to_test(
504504
- a free-threaded wheel is distributed; and
505505
- the SDK supports free-threading.
506506
"""
507+
if not python_versions:
508+
return []
509+
507510
filtered_python_versions = {
508511
python_versions[0],
509512
}
@@ -545,7 +548,8 @@ def _parse_python_versions_from_classifiers(classifiers: list[str]) -> list[Vers
545548

546549
if python_versions:
547550
python_versions.sort()
548-
return python_versions
551+
552+
return python_versions
549553

550554

551555
def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Version]]:
@@ -579,6 +583,14 @@ def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Versi
579583
if requires_python:
580584
return SpecifierSet(requires_python)
581585

586+
# If we haven't found neither specific 3.x classifiers nor a requires_python,
587+
# check if there is a generic "Python 3" classifier and if so, assume the
588+
# package supports all Python versions the SDK does. If this is not the case
589+
# in reality, add the actual constraints manually to config.py.
590+
for classifier in classifiers:
591+
if CLASSIFIER_PREFIX + "3" in classifiers:
592+
return SpecifierSet(f">={MIN_PYTHON_VERSION}")
593+
582594
return []
583595

584596

scripts/populate_tox/releases.jsonl

Lines changed: 37 additions & 40 deletions
Large diffs are not rendered by default.

sentry_sdk/ai/utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,38 @@ class GEN_AI_ALLOWED_MESSAGE_ROLES:
4040
GEN_AI_MESSAGE_ROLE_MAPPING[source_role] = target_role
4141

4242

43+
def parse_data_uri(url: str) -> "Tuple[str, str]":
44+
"""
45+
Parse a data URI and return (mime_type, content).
46+
47+
Data URI format (RFC 2397): data:[<mediatype>][;base64],<data>
48+
49+
Examples:
50+
data:image/jpeg;base64,/9j/4AAQ... → ("image/jpeg", "/9j/4AAQ...")
51+
data:text/plain,Hello → ("text/plain", "Hello")
52+
data:;base64,SGVsbG8= → ("", "SGVsbG8=")
53+
54+
Raises:
55+
ValueError: If the URL is not a valid data URI (missing comma separator)
56+
"""
57+
if "," not in url:
58+
raise ValueError("Invalid data URI: missing comma separator")
59+
60+
header, content = url.split(",", 1)
61+
62+
# Extract mime type from header
63+
# Format: "data:<mime>[;param1][;param2]..." e.g. "data:image/jpeg;base64"
64+
# Remove "data:" prefix, then take everything before the first semicolon
65+
if header.startswith("data:"):
66+
mime_part = header[5:] # Remove "data:" prefix
67+
else:
68+
mime_part = header
69+
70+
mime_type = mime_part.split(";")[0]
71+
72+
return mime_type, content
73+
74+
4375
def _normalize_data(data: "Any", unpack: bool = True) -> "Any":
4476
# convert pydantic data (e.g. OpenAI v1+) to json compatible format
4577
if hasattr(data, "model_dump"):

sentry_sdk/client.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -527,12 +527,21 @@ def _prepare_event(
527527
spans_delta = spans_before - len(
528528
cast(List[Dict[str, object]], event.get("spans", []))
529529
)
530-
if is_transaction and spans_delta > 0 and self.transport is not None:
531-
self.transport.record_lost_event(
532-
"event_processor", data_category="span", quantity=spans_delta
533-
)
530+
span_recorder_dropped_spans: int = event.pop("_dropped_spans", 0)
531+
532+
if is_transaction and self.transport is not None:
533+
if spans_delta > 0:
534+
self.transport.record_lost_event(
535+
"event_processor", data_category="span", quantity=spans_delta
536+
)
537+
if span_recorder_dropped_spans > 0:
538+
self.transport.record_lost_event(
539+
"buffer_overflow",
540+
data_category="span",
541+
quantity=span_recorder_dropped_spans,
542+
)
534543

535-
dropped_spans: int = event.pop("_dropped_spans", 0) + spans_delta
544+
dropped_spans: int = span_recorder_dropped_spans + spans_delta
536545
if dropped_spans > 0:
537546
previous_total_spans = spans_before + dropped_spans
538547
if scope._n_breadcrumbs_truncated > 0:

sentry_sdk/integrations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def iter_default_integrations(
8181
"sentry_sdk.integrations.fastapi.FastApiIntegration",
8282
"sentry_sdk.integrations.flask.FlaskIntegration",
8383
"sentry_sdk.integrations.gql.GQLIntegration",
84+
"sentry_sdk.integrations.google_genai.GoogleGenAIIntegration",
8485
"sentry_sdk.integrations.graphene.GrapheneIntegration",
8586
"sentry_sdk.integrations.httpx.HttpxIntegration",
8687
"sentry_sdk.integrations.huey.HueyIntegration",
@@ -148,6 +149,7 @@ def iter_default_integrations(
148149
"openai_agents": (0, 0, 19),
149150
"openfeature": (0, 7, 1),
150151
"pydantic_ai": (1, 0, 0),
152+
"pymongo": (3, 5, 0),
151153
"quart": (0, 16, 0),
152154
"ray": (2, 7, 0),
153155
"requests": (2, 0, 0),

sentry_sdk/integrations/grpc/__init__.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,29 @@ def __getitem__(self, _):
5050
GRPC_VERSION = parse_version(grpc.__version__)
5151

5252

53+
def _is_channel_intercepted(channel: "Channel") -> bool:
54+
interceptor = getattr(channel, "_interceptor", None)
55+
while interceptor is not None:
56+
if isinstance(interceptor, ClientInterceptor):
57+
return True
58+
59+
inner_channel = getattr(channel, "_channel", None)
60+
if inner_channel is None:
61+
return False
62+
63+
channel = inner_channel
64+
interceptor = getattr(channel, "_interceptor", None)
65+
66+
return False
67+
68+
5369
def _wrap_channel_sync(func: "Callable[P, Channel]") -> "Callable[P, Channel]":
5470
"Wrapper for synchronous secure and insecure channel."
5571

5672
@wraps(func)
5773
def patched_channel(*args: "Any", **kwargs: "Any") -> "Channel":
5874
channel = func(*args, **kwargs)
59-
if not ClientInterceptor._is_intercepted:
60-
ClientInterceptor._is_intercepted = True
75+
if not _is_channel_intercepted(channel):
6176
return intercept_channel(channel, ClientInterceptor())
6277
else:
6378
return channel
@@ -70,7 +85,7 @@ def _wrap_intercept_channel(func: "Callable[P, Channel]") -> "Callable[P, Channe
7085
def patched_intercept_channel(
7186
channel: "Channel", *interceptors: "grpc.ServerInterceptor"
7287
) -> "Channel":
73-
if ClientInterceptor._is_intercepted:
88+
if _is_channel_intercepted(channel):
7489
interceptors = tuple(
7590
[
7691
interceptor

sentry_sdk/integrations/grpc/client.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ class ClientInterceptor(
2222
grpc.UnaryUnaryClientInterceptor, # type: ignore
2323
grpc.UnaryStreamClientInterceptor, # type: ignore
2424
):
25-
_is_intercepted = False
26-
2725
def intercept_unary_unary(
2826
self: "ClientInterceptor",
2927
continuation: "Callable[[ClientCallDetails, Message], _UnaryOutcome]",

0 commit comments

Comments
 (0)