sentry-sdk 3.0.0a5__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.

@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
  import contextlib
3
+ import functools
3
4
  import inspect
4
5
  import os
5
6
  import re
@@ -8,7 +9,6 @@ import uuid
8
9
  from collections.abc import Mapping
9
10
  from datetime import datetime, timedelta, timezone
10
11
  from decimal import ROUND_DOWN, Decimal, DefaultContext, localcontext
11
- from functools import wraps
12
12
  from random import Random
13
13
  from urllib.parse import quote, unquote
14
14
 
@@ -17,6 +17,7 @@ from sentry_sdk.consts import (
17
17
  OP,
18
18
  SPANDATA,
19
19
  SPANSTATUS,
20
+ SPANTEMPLATE,
20
21
  BAGGAGE_HEADER_NAME,
21
22
  SENTRY_TRACE_HEADER_NAME,
22
23
  )
@@ -27,6 +28,7 @@ from sentry_sdk.utils import (
27
28
  logger,
28
29
  match_regex_list,
29
30
  qualname_from_function,
31
+ safe_repr,
30
32
  to_string,
31
33
  is_sentry_url,
32
34
  _is_external_source,
@@ -686,71 +688,117 @@ def normalize_incoming_data(incoming_data: Dict[str, Any]) -> Dict[str, Any]:
686
688
  return data
687
689
 
688
690
 
689
- def start_child_span_decorator(func: Any) -> Any:
691
+ def create_span_decorator(
692
+ op: Optional[Union[str, OP]] = None,
693
+ name: Optional[str] = None,
694
+ attributes: Optional[dict[str, Any]] = None,
695
+ template: SPANTEMPLATE = SPANTEMPLATE.DEFAULT,
696
+ ) -> Any:
690
697
  """
691
- Decorator to add child spans for functions.
692
-
693
- See also ``sentry_sdk.tracing.trace()``.
698
+ Create a span decorator that can wrap both sync and async functions.
699
+
700
+ :param op: The operation type for the span.
701
+ :type op: str or :py:class:`sentry_sdk.consts.OP` or None
702
+ :param name: The name of the span.
703
+ :type name: str or None
704
+ :param attributes: Additional attributes to set on the span.
705
+ :type attributes: dict or None
706
+ :param template: The type of span to create. This determines what kind of
707
+ span instrumentation and data collection will be applied. Use predefined
708
+ constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`.
709
+ The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most
710
+ use cases.
711
+ :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE`
694
712
  """
695
- # Asynchronous case
696
- if inspect.iscoroutinefunction(func):
713
+ from sentry_sdk.scope import should_send_default_pii
714
+
715
+ def span_decorator(f: Any) -> Any:
716
+ """
717
+ Decorator to create a span for the given function.
718
+ """
697
719
 
698
- @wraps(func)
699
- async def func_with_tracing(*args: Any, **kwargs: Any) -> Any:
720
+ @functools.wraps(f)
721
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
700
722
  span = get_current_span()
701
723
 
702
724
  if span is None:
703
725
  logger.debug(
704
726
  "Cannot create a child span for %s. "
705
727
  "Please start a Sentry transaction before calling this function.",
706
- qualname_from_function(func),
728
+ qualname_from_function(f),
707
729
  )
708
- return await func(*args, **kwargs)
730
+ return await f(*args, **kwargs)
731
+
732
+ span_op = op or _get_span_op(template)
733
+ function_name = name or qualname_from_function(f) or ""
734
+ span_name = _get_span_name(template, function_name, kwargs)
735
+ send_pii = should_send_default_pii()
709
736
 
710
737
  with sentry_sdk.start_span(
711
- op=OP.FUNCTION,
712
- name=qualname_from_function(func),
738
+ op=span_op,
739
+ name=span_name,
713
740
  only_as_child_span=True,
714
- ):
715
- return await func(*args, **kwargs)
741
+ ) as span:
742
+ span.set_attributes(attributes or {})
743
+ _set_input_attributes(
744
+ span, template, send_pii, function_name, f, args, kwargs
745
+ )
746
+
747
+ result = await f(*args, **kwargs)
748
+
749
+ _set_output_attributes(span, template, send_pii, result)
750
+
751
+ return result
716
752
 
717
753
  try:
718
- func_with_tracing.__signature__ = inspect.signature( # type: ignore[attr-defined]
719
- func
720
- )
754
+ async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
721
755
  except Exception:
722
756
  pass
723
757
 
724
- # Synchronous case
725
- else:
726
-
727
- @wraps(func)
728
- def func_with_tracing(*args: Any, **kwargs: Any) -> Any:
758
+ @functools.wraps(f)
759
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
729
760
  span = get_current_span()
730
761
 
731
762
  if span is None:
732
763
  logger.debug(
733
764
  "Cannot create a child span for %s. "
734
765
  "Please start a Sentry transaction before calling this function.",
735
- qualname_from_function(func),
766
+ qualname_from_function(f),
736
767
  )
737
- return func(*args, **kwargs)
768
+ return f(*args, **kwargs)
769
+
770
+ span_op = op or _get_span_op(template)
771
+ function_name = name or qualname_from_function(f) or ""
772
+ span_name = _get_span_name(template, function_name, kwargs)
773
+ send_pii = should_send_default_pii()
738
774
 
739
775
  with sentry_sdk.start_span(
740
- op=OP.FUNCTION,
741
- name=qualname_from_function(func),
776
+ op=span_op,
777
+ name=span_name,
742
778
  only_as_child_span=True,
743
- ):
744
- return func(*args, **kwargs)
779
+ ) as span:
780
+ span.set_attributes(attributes or {})
781
+ _set_input_attributes(
782
+ span, template, send_pii, function_name, f, args, kwargs
783
+ )
784
+
785
+ result = f(*args, **kwargs)
786
+
787
+ _set_output_attributes(span, template, send_pii, result)
788
+
789
+ return result
745
790
 
746
791
  try:
747
- func_with_tracing.__signature__ = inspect.signature( # type: ignore[attr-defined]
748
- func
749
- )
792
+ sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
750
793
  except Exception:
751
794
  pass
752
795
 
753
- return func_with_tracing
796
+ if inspect.iscoroutinefunction(f):
797
+ return async_wrapper
798
+ else:
799
+ return sync_wrapper
800
+
801
+ return span_decorator
754
802
 
755
803
 
756
804
  def get_current_span(
@@ -813,6 +861,255 @@ def _sample_rand_range(
813
861
  return sample_rate, 1.0
814
862
 
815
863
 
864
+ def _get_value(source: Any, key: str) -> Optional[Any]:
865
+ """
866
+ Gets a value from a source object. The source can be a dict or an object.
867
+ It is checked for dictionary keys and object attributes.
868
+ """
869
+ value = None
870
+ if isinstance(source, dict):
871
+ value = source.get(key)
872
+ else:
873
+ if hasattr(source, key):
874
+ try:
875
+ value = getattr(source, key)
876
+ except Exception:
877
+ value = None
878
+ return value
879
+
880
+
881
+ def _get_span_name(
882
+ template: Union[str, SPANTEMPLATE],
883
+ name: str,
884
+ kwargs: Optional[dict[str, Any]] = None,
885
+ ) -> str:
886
+ """
887
+ Get the name of the span based on the template and the name.
888
+ """
889
+ span_name = name
890
+
891
+ if template == SPANTEMPLATE.AI_CHAT:
892
+ model = None
893
+ if kwargs:
894
+ for key in ("model", "model_name"):
895
+ if kwargs.get(key) and isinstance(kwargs[key], str):
896
+ model = kwargs[key]
897
+ break
898
+
899
+ span_name = f"chat {model}" if model else "chat"
900
+
901
+ elif template == SPANTEMPLATE.AI_AGENT:
902
+ span_name = f"invoke_agent {name}"
903
+
904
+ elif template == SPANTEMPLATE.AI_TOOL:
905
+ span_name = f"execute_tool {name}"
906
+
907
+ return span_name
908
+
909
+
910
+ def _get_span_op(template: Union[str, SPANTEMPLATE]) -> str:
911
+ """
912
+ Get the operation of the span based on the template.
913
+ """
914
+ mapping = {
915
+ SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT,
916
+ SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT,
917
+ SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL,
918
+ } # type: dict[Union[str, SPANTEMPLATE], Union[str, OP]]
919
+ op = mapping.get(template, OP.FUNCTION)
920
+
921
+ return str(op)
922
+
923
+
924
+ def _get_input_attributes(
925
+ template: Union[str, SPANTEMPLATE],
926
+ send_pii: bool,
927
+ args: tuple[Any, ...],
928
+ kwargs: dict[str, Any],
929
+ ) -> dict[str, Any]:
930
+ """
931
+ Get input attributes for the given span template.
932
+ """
933
+ attributes: dict[str, Any] = {}
934
+
935
+ if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]:
936
+ mapping: dict[str, tuple[str, type]] = {
937
+ "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str),
938
+ "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str),
939
+ "agent": (SPANDATA.GEN_AI_AGENT_NAME, str),
940
+ "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str),
941
+ "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int),
942
+ "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float),
943
+ "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float),
944
+ "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float),
945
+ "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float),
946
+ "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int),
947
+ }
948
+
949
+ def _set_from_key(key: str, value: Any) -> None:
950
+ if key in mapping:
951
+ (attribute, data_type) = mapping[key]
952
+ if value is not None and isinstance(value, data_type):
953
+ attributes[attribute] = value
954
+
955
+ for key, value in list(kwargs.items()):
956
+ if key == "prompt" and isinstance(value, str):
957
+ attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append(
958
+ {"role": "user", "content": value}
959
+ )
960
+ continue
961
+
962
+ if key == "system_prompt" and isinstance(value, str):
963
+ attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append(
964
+ {"role": "system", "content": value}
965
+ )
966
+ continue
967
+
968
+ _set_from_key(key, value)
969
+
970
+ if template == SPANTEMPLATE.AI_TOOL and send_pii:
971
+ attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr(
972
+ {"args": args, "kwargs": kwargs}
973
+ )
974
+
975
+ # Coerce to string
976
+ if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes:
977
+ attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr(
978
+ attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES]
979
+ )
980
+
981
+ return attributes
982
+
983
+
984
+ def _get_usage_attributes(usage: Any) -> dict[str, Any]:
985
+ """
986
+ Get usage attributes.
987
+ """
988
+ attributes = {}
989
+
990
+ def _set_from_keys(attribute: str, keys: tuple[str, ...]) -> None:
991
+ for key in keys:
992
+ value = _get_value(usage, key)
993
+ if value is not None and isinstance(value, int):
994
+ attributes[attribute] = value
995
+
996
+ _set_from_keys(
997
+ SPANDATA.GEN_AI_USAGE_INPUT_TOKENS,
998
+ ("prompt_tokens", "input_tokens"),
999
+ )
1000
+ _set_from_keys(
1001
+ SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS,
1002
+ ("completion_tokens", "output_tokens"),
1003
+ )
1004
+ _set_from_keys(
1005
+ SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS,
1006
+ ("total_tokens",),
1007
+ )
1008
+
1009
+ return attributes
1010
+
1011
+
1012
+ def _get_output_attributes(
1013
+ template: Union[str, SPANTEMPLATE], send_pii: bool, result: Any
1014
+ ) -> dict[str, Any]:
1015
+ """
1016
+ Get output attributes for the given span template.
1017
+ """
1018
+ attributes = {} # type: dict[str, Any]
1019
+
1020
+ if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]:
1021
+ with capture_internal_exceptions():
1022
+ # Usage from result, result.usage, and result.metadata.usage
1023
+ usage_candidates = [result]
1024
+
1025
+ usage = _get_value(result, "usage")
1026
+ usage_candidates.append(usage)
1027
+
1028
+ meta = _get_value(result, "metadata")
1029
+ usage = _get_value(meta, "usage")
1030
+ usage_candidates.append(usage)
1031
+
1032
+ for usage_candidate in usage_candidates:
1033
+ if usage_candidate is not None:
1034
+ attributes.update(_get_usage_attributes(usage_candidate))
1035
+
1036
+ # Response model
1037
+ model_name = _get_value(result, "model")
1038
+ if model_name is not None and isinstance(model_name, str):
1039
+ attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name
1040
+
1041
+ model_name = _get_value(result, "model_name")
1042
+ if model_name is not None and isinstance(model_name, str):
1043
+ attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name
1044
+
1045
+ # Tool output
1046
+ if template == SPANTEMPLATE.AI_TOOL and send_pii:
1047
+ attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result)
1048
+
1049
+ return attributes
1050
+
1051
+
1052
+ def _set_input_attributes(
1053
+ span: sentry_sdk.tracing.Span,
1054
+ template: Union[str, SPANTEMPLATE],
1055
+ send_pii: bool,
1056
+ name: str,
1057
+ f: Any,
1058
+ args: tuple[Any, ...],
1059
+ kwargs: dict[str, Any],
1060
+ ) -> None:
1061
+ """
1062
+ Set span input attributes based on the given span template.
1063
+
1064
+ :param span: The span to set attributes on.
1065
+ :param template: The template to use to set attributes on the span.
1066
+ :param send_pii: Whether to send PII data.
1067
+ :param f: The wrapped function.
1068
+ :param args: The arguments to the wrapped function.
1069
+ :param kwargs: The keyword arguments to the wrapped function.
1070
+ """
1071
+ attributes = {} # type: dict[str, Any]
1072
+
1073
+ if template == SPANTEMPLATE.AI_AGENT:
1074
+ attributes = {
1075
+ SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent",
1076
+ SPANDATA.GEN_AI_AGENT_NAME: name,
1077
+ }
1078
+ elif template == SPANTEMPLATE.AI_CHAT:
1079
+ attributes = {
1080
+ SPANDATA.GEN_AI_OPERATION_NAME: "chat",
1081
+ }
1082
+ elif template == SPANTEMPLATE.AI_TOOL:
1083
+ attributes = {
1084
+ SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
1085
+ SPANDATA.GEN_AI_TOOL_NAME: name,
1086
+ }
1087
+
1088
+ docstring = f.__doc__
1089
+ if docstring is not None:
1090
+ attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring
1091
+
1092
+ attributes.update(_get_input_attributes(template, send_pii, args, kwargs))
1093
+ span.set_attributes(attributes or {})
1094
+
1095
+
1096
+ def _set_output_attributes(
1097
+ span: sentry_sdk.tracing.Span,
1098
+ template: Union[str, SPANTEMPLATE],
1099
+ send_pii: bool,
1100
+ result: Any,
1101
+ ) -> None:
1102
+ """
1103
+ Set span output attributes based on the given span template.
1104
+
1105
+ :param span: The span to set attributes on.
1106
+ :param template: The template to use to set attributes on the span.
1107
+ :param send_pii: Whether to send PII data.
1108
+ :param result: The result of the wrapped function.
1109
+ """
1110
+ span.set_attributes(_get_output_attributes(template, send_pii, result) or {})
1111
+
1112
+
816
1113
  def get_span_status_from_http_code(http_status_code: int) -> str:
817
1114
  """
818
1115
  Returns the Sentry status corresponding to the given HTTP status code.