Skip to content

Commit 852402e

Browse files
Merge branch 'master' into constantinius/feat/integrations/openai-agents-conversation-id
2 parents 621766c + ae502a4 commit 852402e

File tree

18 files changed

+525
-141
lines changed

18 files changed

+525
-141
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/_types.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class SDKInfo(TypedDict):
256256
)
257257

258258
MetricType = Literal["counter", "gauge", "distribution"]
259+
MetricUnit = Union[DurationUnit, InformationUnit, str]
259260

260261
Metric = TypedDict(
261262
"Metric",
@@ -266,7 +267,7 @@ class SDKInfo(TypedDict):
266267
"name": str,
267268
"type": MetricType,
268269
"value": float,
269-
"unit": Optional[str],
270+
"unit": Optional[MetricUnit],
270271
"attributes": Attributes,
271272
},
272273
)

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: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None:
188188
self.monitor: "Optional[Monitor]" = None
189189
self.log_batcher: "Optional[LogBatcher]" = None
190190
self.metrics_batcher: "Optional[MetricsBatcher]" = None
191+
self.integrations: "dict[str, Integration]" = {}
191192

192193
def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any":
193194
return {"options": {}}
@@ -526,12 +527,21 @@ def _prepare_event(
526527
spans_delta = spans_before - len(
527528
cast(List[Dict[str, object]], event.get("spans", []))
528529
)
529-
if is_transaction and spans_delta > 0 and self.transport is not None:
530-
self.transport.record_lost_event(
531-
"event_processor", data_category="span", quantity=spans_delta
532-
)
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+
)
533543

534-
dropped_spans: int = event.pop("_dropped_spans", 0) + spans_delta
544+
dropped_spans: int = span_recorder_dropped_spans + spans_delta
535545
if dropped_spans > 0:
536546
previous_total_spans = spans_before + dropped_spans
537547
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/asyncio.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def patch_asyncio() -> None:
4747
loop = asyncio.get_running_loop()
4848
orig_task_factory = loop.get_task_factory()
4949

50+
# Check if already patched
51+
if getattr(orig_task_factory, "_is_sentry_task_factory", False):
52+
return
53+
5054
def _sentry_task_factory(
5155
loop: "asyncio.AbstractEventLoop",
5256
coro: "Coroutine[Any, Any, Any]",
@@ -102,6 +106,7 @@ async def _task_with_sentry_span_creation() -> "Any":
102106

103107
return task
104108

109+
_sentry_task_factory._is_sentry_task_factory = True # type: ignore
105110
loop.set_task_factory(_sentry_task_factory) # type: ignore
106111

107112
except RuntimeError:
@@ -138,3 +143,48 @@ class AsyncioIntegration(Integration):
138143
@staticmethod
139144
def setup_once() -> None:
140145
patch_asyncio()
146+
147+
148+
def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None:
149+
"""
150+
Enable AsyncioIntegration with the provided options.
151+
152+
This is useful in scenarios where Sentry needs to be initialized before
153+
an event loop is set up, but you still want to instrument asyncio once there
154+
is an event loop. In that case, you can sentry_sdk.init() early on without
155+
the AsyncioIntegration and then, once the event loop has been set up,
156+
execute:
157+
158+
```python
159+
from sentry_sdk.integrations.asyncio import enable_asyncio_integration
160+
161+
async def async_entrypoint():
162+
enable_asyncio_integration()
163+
```
164+
165+
Any arguments provided will be passed to AsyncioIntegration() as is.
166+
167+
If AsyncioIntegration has already patched the current event loop, this
168+
function won't have any effect.
169+
170+
If AsyncioIntegration was provided in
171+
sentry_sdk.init(disabled_integrations=[...]), this function will ignore that
172+
and the integration will be enabled.
173+
"""
174+
client = sentry_sdk.get_client()
175+
if not client.is_active():
176+
return
177+
178+
# This function purposefully bypasses the integration machinery in
179+
# integrations/__init__.py. _installed_integrations/_processed_integrations
180+
# is used to prevent double patching the same module, but in the case of
181+
# the AsyncioIntegration, we don't monkeypatch the standard library directly,
182+
# we patch the currently running event loop, and we keep the record of doing
183+
# that on the loop itself.
184+
logger.debug("Setting up integration asyncio")
185+
186+
integration = AsyncioIntegration(*args, **kwargs)
187+
integration.setup_once()
188+
189+
if "asyncio" not in client.integrations:
190+
client.integrations["asyncio"] = integration

0 commit comments

Comments
 (0)