Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,58 @@ Want to give it a spin? Be our guest and [let us know](https://github.com/getsen
- [2.x to 3.x migration guide](https://docs.sentry.io/platforms/python/migration/2.x-to-3.x)
- [detailed changelog](https://github.com/getsentry/sentry-python/blob/major/3.0/MIGRATION_GUIDE.md)

## 2.74.0

### New Features ✨

#### Anthropic

- Record reasoning parts in streaming output by @alexander-alderman-webb in [#8193](https://github.com/getsentry/sentry-python/pull/8193)
- Record reasoning parts in nonstreaming output by @alexander-alderman-webb in [#8192](https://github.com/getsentry/sentry-python/pull/8192)

#### Other

- (typesafe) Handle inline images by @alexander-alderman-webb in [#8164](https://github.com/getsentry/sentry-python/pull/8164)

### Bug Fixes 🐛

#### Anthropic

- Preserve cached `_iterator` property by @alexander-alderman-webb in [#8163](https://github.com/getsentry/sentry-python/pull/8163)
- Stop unpacking output arrays by @alexander-alderman-webb in [#8160](https://github.com/getsentry/sentry-python/pull/8160)

#### Other

- (aiohttp) Leave existing `baggage` header alone if it has existin… by @pabloDeputter in [#8116](https://github.com/getsentry/sentry-python/pull/8116)
- (boto3) Adapt to botocore no longer signing baggage by @sentrivana in [#8113](https://github.com/getsentry/sentry-python/pull/8113)
- (ci) Restrict pytest because of incompatibilities by @sentrivana in [#8140](https://github.com/getsentry/sentry-python/pull/8140)
- (falcon) Handle integer HTTP statuses by @jap and @sentrivana in [#8139](https://github.com/getsentry/sentry-python/pull/8139)
- (sampling) Respect incoming sample rate if not explicitly overridden by @sentrivana in [#8185](https://github.com/getsentry/sentry-python/pull/8185)
- (tracing) Drop trailing comma when serializing baggage without third-party items by @alphasafal and @pabloDeputter in [#8119](https://github.com/getsentry/sentry-python/pull/8119)
- (wsgi) Stop overwriting `server.address` by @alexander-alderman-webb in [#8171](https://github.com/getsentry/sentry-python/pull/8171)

### Internal Changes 🔧

#### Deps

- Lock file maintenance by @renovate in [#8055](https://github.com/getsentry/sentry-python/pull/8055)
- Bump grpcio to 1.84.1 by @geoffg-sentry with @sentry-junior in [#8136](https://github.com/getsentry/sentry-python/pull/8136)
- Bump uv to 0.13.5 by @geoffg-sentry with @sentry-junior in [#8135](https://github.com/getsentry/sentry-python/pull/8135)
- Bump brotli to 1.2.1 by @geoffg-sentry with @sentry-junior in [#8122](https://github.com/getsentry/sentry-python/pull/8122)

#### Other

- (mcp) Remove unused select_mcp_transactions fixture by @sentrivana with @sentry-junior in [#8150](https://github.com/getsentry/sentry-python/pull/8150)
- (release) Bump craft to 3.9.0 and update config by @BYK with @sentry-junior in [#8181](https://github.com/getsentry/sentry-python/pull/8181)
- (sessions) Remove unused TERMINAL_SESSION_STATES constant by @sentrivana with @sentry-junior in [#8151](https://github.com/getsentry/sentry-python/pull/8151)
- (starlette) Remove unused \_serialize_request_body_data helper by @sentry-junior in [#8146](https://github.com/getsentry/sentry-python/pull/8146)
- (toxgen) Remove unused `get_file_hash()` and `get_last_updated()` helpers by @sentry-junior in [#8173](https://github.com/getsentry/sentry-python/pull/8173)
- (tracing) Remove unused base64_stripped regex by @sentry-junior in [#8148](https://github.com/getsentry/sentry-python/pull/8148)
- (utils) Remove unused to_timestamp helper and epoch constant by @sentry-junior in [#8147](https://github.com/getsentry/sentry-python/pull/8147)
- 🤖 Update test matrix with new releases (11/02) by @sentrivana with @github-actions in [#8172](https://github.com/getsentry/sentry-python/pull/8172)
- Upgrade `sphinx` and remove `packaging` upper bound by @alexander-alderman-webb in [#8183](https://github.com/getsentry/sentry-python/pull/8183)
- Update test matrix with new releases (27/10) by @alexander-alderman-webb in [#8126](https://github.com/getsentry/sentry-python/pull/8126)

## 2.73.0

### New Features ✨
Expand Down
2 changes: 2 additions & 0 deletions sentry_sdk/crons/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ async def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
with self:
return await fn(*args, **kwargs)

inner._sentry_monitored = True # type: ignore[attr-defined]
return inner

def _sync_wrapper(self, fn: "Callable[P, R]") -> "Callable[P, R]":
Expand All @@ -134,4 +135,5 @@ def inner(*args: "P.args", **kwargs: "P.kwargs") -> "R":
with self:
return fn(*args, **kwargs)

inner._sentry_monitored = True # type: ignore[attr-defined]
return inner
26 changes: 26 additions & 0 deletions sentry_sdk/crons/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import re
from datetime import timedelta, timezone
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from datetime import tzinfo
from typing import Optional, Tuple

from sentry_sdk._types import MonitorConfigSchedule, MonitorConfigScheduleUnit
Expand All @@ -27,3 +30,26 @@ def _get_interval_schedule(seconds: float) -> "Optional[MonitorConfigSchedule]":
"unit": unit,
}
return None


# IANA zone names start with an uppercase letter. This rejects names like
# "local" or "localtime" that tzlocal can give a zone.
_IANA_TIMEZONE_NAME = re.compile(r"[A-Z][A-Za-z0-9._+-]*(?:/[A-Za-z0-9._+-]+)*")


def _get_timezone_name(tz: "tzinfo") -> "Optional[str]":
"""
Return the IANA name of the timezone, or None if it has none.
"""
name = getattr(tz, "key", None) or getattr(tz, "zone", None)
if isinstance(name, str):
return name if _IANA_TIMEZONE_NAME.fullmatch(name) else None

if isinstance(tz, timezone):
offset = tz.utcoffset(None)
if offset % timedelta(hours=1):
return None
hours = offset // timedelta(hours=1)
return "Etc/GMT%+d" % -hours if hours else "UTC"

return None
66 changes: 41 additions & 25 deletions sentry_sdk/integrations/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
except ImportError:
raise DidNotEnable("Anthropic not installed or incompatible")

try:
from anthropic.types import ThinkingBlock, ThinkingDelta
except ImportError:
ThinkingBlock = None # type: ignore[assignment,misc]
ThinkingDelta = None # type: ignore[assignment,misc]

if TYPE_CHECKING:
from typing import (
Any,
Expand All @@ -77,15 +83,15 @@
ToolUnionParam,
)

from sentry_sdk._types import TextPart
from sentry_sdk._types import ReasoningPart, TextPart

class _PatchedRawMessageStream(Stream[Any]):
_span: Span
_integration: "AnthropicIntegration"

_model: Optional[ModelParam]
_usage: "_RecordedUsage"
_content_blocks: list[Any]
_content_blocks: "list[dict[str, Any]]"
_response_id: Optional[str]
_finish_reason: Optional[str]

Expand All @@ -95,7 +101,7 @@ class _PatchedMessageStream(MessageStream):

_model: Optional[ModelParam]
_usage: "_RecordedUsage"
_content_blocks: list[Any]
_content_blocks: "list[dict[str, Any]]"
_response_id: Optional[str]
_finish_reason: Optional[str]

Expand All @@ -105,7 +111,7 @@ class _PatchedRawAsyncMessageStream(AsyncStream[Any]):

_model: Optional[ModelParam]
_usage: "_RecordedUsage"
_content_blocks: list[Any]
_content_blocks: "list[dict[str, Any]]"
_response_id: Optional[str]
_finish_reason: Optional[str]

Expand All @@ -115,7 +121,7 @@ class _PatchedAsyncMessageStream(AsyncMessageStream):

_model: Optional[ModelParam]
_usage: "_RecordedUsage"
_content_blocks: list[Any]
_content_blocks: "list[dict[str, Any]]"
_response_id: Optional[str]
_finish_reason: Optional[str]

Expand Down Expand Up @@ -353,22 +359,32 @@ def _collect_ai_data(
event: "MessageStreamEvent",
model: "str | None",
usage: "_RecordedUsage",
content_blocks: "list[str]",
content_blocks: "list[dict[str, Any]]",
response_id: "str | None" = None,
finish_reason: "str | None" = None,
) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]":
) -> "tuple[str | None, _RecordedUsage, list[dict[str, Any]], str | None, str | None]":
"""
Collect model information, token usage, and collect content blocks from the AI streaming response.
"""
with capture_internal_exceptions():
if hasattr(event, "type"):
if event.type == "content_block_start":
pass
if ThinkingBlock is not None and isinstance(
event.content_block, ThinkingBlock
):
content_blocks.append({"type": "thinking", "thinking": ""})
elif event.content_block.type in ("text", "tool_use"):
content_blocks.append({"type": "text", "text": ""})
else:
content_blocks.append({"type": event.content_block.type})
elif event.type == "content_block_delta":
if hasattr(event.delta, "text"):
content_blocks.append(event.delta.text)
block = content_blocks[event.index]
if ThinkingDelta is not None and isinstance(event.delta, ThinkingDelta):
block["thinking"] += event.delta.thinking
elif hasattr(event.delta, "text"):
block["text"] += event.delta.text
elif hasattr(event.delta, "partial_json"):
content_blocks.append(event.delta.partial_json)
block["text"] += event.delta.partial_json
elif event.type == "content_block_stop":
pass

Expand Down Expand Up @@ -664,7 +680,7 @@ def _set_output_data(
output_tokens: "int | None",
cache_read_input_tokens: "int | None",
cache_write_input_tokens: "int | None",
content_blocks: "list[Any]",
content_blocks: "list[dict[str, Any]]",
response_id: "str | None" = None,
finish_reason: "str | None" = None,
) -> None:
Expand All @@ -678,30 +694,30 @@ def _set_output_data(
span.set_attribute(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason])

if sentry_sdk.get_client().options["data_collection"]["gen_ai"]["outputs"]:
output_messages: "dict[str, list[Any]]" = {
"response": [],
"tool": [],
}
parts: "list[Union[TextPart, ReasoningPart]]" = []
tool_calls = []

for output in content_blocks:
if output["type"] == "text":
output_messages["response"].append(output["text"])
parts.append({"type": "text", "content": output["text"]})
elif output["type"] == "thinking":
parts.append({"type": "reasoning", "content": output["thinking"]})
elif output["type"] == "tool_use":
output_messages["tool"].append(output)
tool_calls.append(output)

if len(output_messages["tool"]) > 0:
if len(tool_calls) > 0:
set_data_normalized(
span,
SPANDATA.GEN_AI_RESPONSE_TOOL_CALLS,
output_messages["tool"],
tool_calls,
unpack=False,
)

if len(output_messages["response"]) > 0:
if len(parts) > 0:
set_data_normalized(
span,
SPANDATA.GEN_AI_RESPONSE_TEXT,
output_messages["response"],
SPANDATA.GEN_AI_OUTPUT_MESSAGES,
[{"role": "assistant", "parts": parts}],
unpack=False,
)

Expand Down Expand Up @@ -977,7 +993,7 @@ def _set_streaming_output_data(
integration: "AnthropicIntegration",
model: "Optional[str]",
usage: "_RecordedUsage",
content_blocks: "list[str]",
content_blocks: "list[dict[str, Any]]",
response_id: "Optional[str]",
finish_reason: "Optional[str]",
) -> None:
Expand All @@ -1000,7 +1016,7 @@ def _set_streaming_output_data(
output_tokens=usage.output_tokens,
cache_read_input_tokens=usage.cache_read_input_tokens,
cache_write_input_tokens=usage.cache_write_input_tokens,
content_blocks=[{"text": "".join(content_blocks), "type": "text"}],
content_blocks=content_blocks,
response_id=response_id,
finish_reason=finish_reason,
)
Expand Down
25 changes: 9 additions & 16 deletions sentry_sdk/integrations/arq.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import sys
import time
from datetime import timedelta, timezone
from functools import wraps

import sentry_sdk
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.crons import monitor
from sentry_sdk.crons.utils import _get_timezone_name
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
from sentry_sdk.integrations.logging import ignore_logger_for_events
from sentry_sdk.traces import SegmentNameSource
Expand Down Expand Up @@ -135,15 +136,15 @@
Worker.run_job = _sentry_run_job


def _capture_exception(exc_info: "ExcInfo") -> None:
if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS:
return

event, hint = event_from_exception(
exc_info,
mechanism={"type": ArqIntegration.identifier, "handled": False},
)
sentry_sdk.capture_event(event, hint=hint)

Check warning on line 147 in sentry_sdk/integrations/arq.py

View check run for this annotation

@sentry/warden / warden: code-review

Failed arq jobs leave the queue process span marked OK

For ordinary job failures, `_capture_exception` captures the exception but does not mark the active span or segment as an error. The span created around `Worker.run_job` defaults to `ok`, and arq handles the re-raised job exception internally, so the context manager does not see it. Set the current segment status to error when capturing a real failure.


def _make_event_processor(
Expand Down Expand Up @@ -174,6 +175,7 @@


def _wrap_coroutine(name: str, coroutine: "WorkerCoroutine") -> "WorkerCoroutine":
@wraps(coroutine)
async def _sentry_coroutine(
ctx: "Dict[Any, Any]", *args: "Any", **kwargs: "Any"
) -> "Any":
Expand Down Expand Up @@ -278,6 +280,9 @@
return
cron_job._sentry_monitored = True

if getattr(cron_job.coroutine, "_sentry_monitored", False):
return

# arq names cron jobs "cron:<function name>" by default.
monitor_slug = cron_job.name
if monitor_slug.startswith("cron:"):
Expand Down Expand Up @@ -345,35 +350,23 @@
return " ".join(fields) # type: ignore[arg-type]


def _get_timezone_name(worker: "Worker") -> "Optional[str]":
def _get_worker_timezone_name(worker: "Worker") -> "Optional[str]":
tz = getattr(worker, "timezone", None)
if tz is None:
# Before arq 0.25, cron jobs ran in naive local time.
if time.timezone == 0 and (not time.daylight or time.altzone == 0):
return "UTC"
return None

# zoneinfo and pytz
name = getattr(tz, "key", None) or getattr(tz, "zone", None)
if isinstance(name, str):
return name

# arq defaults to the system's fixed UTC offset at startup.
if isinstance(tz, timezone):
offset = tz.utcoffset(None)
if offset % timedelta(hours=1):
return None
hours = offset // timedelta(hours=1)
return "Etc/GMT%+d" % -hours if hours else "UTC"

return None
return _get_timezone_name(tz)


def _get_monitor_config(
cron_job: "CronJob", worker: "Worker"
) -> "Optional[MonitorConfig]":
crontab = _get_crontab(cron_job)
timezone_name = _get_timezone_name(worker)
timezone_name = _get_worker_timezone_name(worker)
if crontab is None or timezone_name is None:
return None
return {
Expand Down
3 changes: 0 additions & 3 deletions sentry_sdk/profiler/continuous_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,6 @@ def ensure_running(self) -> None:
def teardown(self) -> None:
raise NotImplementedError

def pause(self) -> None:
raise NotImplementedError

def reset_buffer(self) -> None:
self.buffer = ProfileBuffer(
self.options, self.sdk_info, PROFILE_BUFFER_SECONDS, self.capture_func
Expand Down
7 changes: 6 additions & 1 deletion sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,7 +1012,12 @@ def start_span(
self,
)

if sample_rate is not None:
keep_incoming_sample_rate = (
propagation_context.parent_sampled is not None
and not callable(self.get_client().options.get("traces_sampler"))
)

if sample_rate is not None and not keep_incoming_sample_rate:
self._update_sample_rate(sample_rate)

if sampled is False or sampled is None:
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __str__(self) -> str:


# The source of a segment name, see
# https://getsentry.github.io/sentry-conventions/attributes/sentry/#sentry-segment-name-source
# https://conventions.sentry.dev/attributes/sentry/#sentry-segment-name-source
class SegmentNameSource(str, Enum):
COMPONENT = "component"
CUSTOM = "custom"
Expand Down
Loading
Loading