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
6 changes: 6 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,12 @@ class SPANDATA:
Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}]
"""

GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK = "gen_ai.response.time_to_first_chunk"
"""
Time in seconds when the first response content chunk arrived in streaming responses.
Example: 0.6853435
"""

GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token"
"""
The time it took to receive the first token from the model.
Expand Down
3 changes: 3 additions & 0 deletions sentry_sdk/integrations/boto3/_client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import time
from contextlib import contextmanager
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -116,6 +117,8 @@ def sentry_patched_make_api_call(

attributes: "Attributes" = {}
try:
if service_ext is not None:
service_ext.request_start_time = time.perf_counter()
with span_ctx:
try:
parsed = orig_make_api_call(self, operation_name, api_params)
Expand Down
5 changes: 4 additions & 1 deletion sentry_sdk/integrations/boto3/_services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ class _ServiceExtension:
to use the generic instrumentation.
"""

__slots__ = ()
__slots__ = "request_start_time"
skip_if_no_active_span = True

def __init__(self) -> None:
self.request_start_time: "Optional[float]" = None

def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]":
"""Return an optional `sentry.op` override, or `None` to keep the default."""
return None
Expand Down
21 changes: 20 additions & 1 deletion sentry_sdk/integrations/boto3/_services/bedrock.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import sys
import time
from typing import TYPE_CHECKING, cast

import sentry_sdk
Expand Down Expand Up @@ -44,6 +45,10 @@
from sentry_sdk.integrations.boto3._context import AwsCallContext
from sentry_sdk.tracing import Span

class _PatchedEventStream(EventStream[Any]):
_span: "Union[Span, StreamedSpan]"
_request_start_time: "Optional[float]"


def _finish_span(
stream: "EventStream[Any]",
Expand Down Expand Up @@ -89,6 +94,8 @@ def sentry_patched_iter(
yield from original_iter(self)
return

self = cast("_PatchedEventStream", self)

set_on_span = (
span.set_attribute if isinstance(span, StreamedSpan) else span.set_data
)
Expand All @@ -97,6 +104,7 @@ def sentry_patched_iter(
parts: "dict[int, Union[TextPart, ReasoningPart, ToolCallPart, ToolCallResponsePart, UriPart, BlobPart]]" = {}

try:
start_time: "Optional[float]" = self._request_start_time
for event in original_iter(self):
if "metadata" in event:
usage = event["metadata"]["usage"]
Expand Down Expand Up @@ -132,6 +140,13 @@ def sentry_patched_iter(
yield event
continue

if start_time is not None and "contentBlockDelta" in event:
set_on_span(
SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK,
time.perf_counter() - start_time,
)
start_time = None
Comment thread
alexander-alderman-webb marked this conversation as resolved.
Resolved

if "messageStop" in event:
finish_reasons: "list[str]" = [event["messageStop"]["stopReason"]]
set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons)
Expand Down Expand Up @@ -602,9 +617,12 @@ def _transform_tool_definitions(


class _BedrockExtension(_ServiceExtension):
__slots__ = ()
__slots__ = "request_start_time"
skip_if_no_active_span = False

def __init__(self) -> None:
self.request_start_time: "Optional[float]" = None

def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]":
if ctx.operation_name != "Converse" and ctx.operation_name != "ConverseStream":
return None
Expand Down Expand Up @@ -743,6 +761,7 @@ def on_success_response(
if ctx.operation_name == "ConverseStream":
stream = cast("ConverseStreamResponseTypeDef", response)["stream"]
stream._span = span # type: ignore[attr-defined]
stream._request_start_time = self.request_start_time # type: ignore[attr-defined]
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
return

if isinstance(span, StreamedSpan):
Expand Down
2 changes: 2 additions & 0 deletions tests/integrations/boto3/test_bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,7 @@ def test_converse_stream(
assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [
"end_turn"
]
assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK in span["attributes"]

assert span["attributes"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
Expand Down Expand Up @@ -1035,6 +1036,7 @@ def test_converse_stream(
assert span["data"][SPANDATA.GEN_AI_REQUEST_TEMPERATURE] == 0.7
assert span["data"][SPANDATA.GEN_AI_REQUEST_TOP_P] == 0.9
assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["end_turn"]
assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK in span["data"]

assert span["data"][SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 20
assert span["data"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 30
Expand Down
Loading