diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index bee14e8ee5..e1a84cc240 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -79,7 +79,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} @@ -145,7 +145,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index d0fd42d35f..65f304ed05 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -79,7 +79,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} @@ -145,7 +145,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index 820f7cc3f5..3405d401fb 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -59,7 +59,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index 36a8ee8025..2ba282a85e 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -99,7 +99,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} @@ -185,7 +185,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-flags.yml b/.github/workflows/test-integrations-flags.yml index 35d4b14ef6..07f97b4e00 100644 --- a/.github/workflows/test-integrations-flags.yml +++ b/.github/workflows/test-integrations-flags.yml @@ -71,7 +71,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-gevent.yml b/.github/workflows/test-integrations-gevent.yml index 564659fe0c..2b41a0b9c8 100644 --- a/.github/workflows/test-integrations-gevent.yml +++ b/.github/workflows/test-integrations-gevent.yml @@ -59,7 +59,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 5832312069..54fca489b9 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -71,7 +71,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index 7da69ff9b6..e77f138546 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -79,7 +79,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index 4aea25bcfe..edf1857194 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -67,7 +67,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} @@ -121,7 +121,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index c80d1f0c0a..009fbcab13 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -94,7 +94,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} @@ -175,7 +175,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index ff14f7306e..7f3e1b104b 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -89,7 +89,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index f454ba9c69..2477e5925f 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -95,7 +95,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} @@ -177,7 +177,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov if: ${{ !cancelled() }} diff --git a/docs/api.rst b/docs/api.rst index f79eed0cbb..c09b5e9468 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,6 +41,7 @@ Performance Monitoring .. autofunction:: sentry_sdk.api.get_current_span .. autofunction:: sentry_sdk.api.start_span .. autofunction:: sentry_sdk.api.start_transaction +.. autofunction:: sentry_sdk.api.update_current_span Distributed Tracing diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index 1691b467ae..e78e0ece67 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -91,7 +91,7 @@ token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} files: coverage.xml # make sure no plugins alter our coverage reports - plugin: noop + plugins: noop verbose: true - name: Upload test results to Codecov diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index b0db75f254..51916580db 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -49,6 +49,7 @@ "start_session", "end_session", "set_transaction_name", + "update_current_span", ] # Initialize the debug support after everything is loaded diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 089e569dfa..ea6a678ee1 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -76,6 +76,7 @@ "start_session", "end_session", "set_transaction_name", + "update_current_span", ] @@ -341,3 +342,59 @@ def end_session() -> None: @scopemethod def set_transaction_name(name: str, source: Optional[str] = None) -> None: return get_current_scope().set_transaction_name(name, source) + + +def update_current_span(op=None, name=None, attributes=None): + # type: (Optional[str], Optional[str], Optional[dict[str, Union[str, int, float, bool]]]) -> None + """ + Update the current active span with the provided parameters. + + This function allows you to modify properties of the currently active span. + If no span is currently active, this function will do nothing. + + :param op: The operation name for the span. This is a high-level description + of what the span represents (e.g., "http.client", "db.query"). + You can use predefined constants from :py:class:`sentry_sdk.consts.OP` + or provide your own string. If not provided, the span's operation will + remain unchanged. + :type op: str or None + + :param name: The human-readable name/description for the span. This provides + more specific details about what the span represents (e.g., "GET /api/users", + "SELECT * FROM users"). If not provided, the span's name will remain unchanged. + :type name: str or None + + :param attributes: A dictionary of key-value pairs to add as attributes to the span. + Attribute values must be strings, integers, floats, or booleans. These + attributes will be merged with any existing span data. If not provided, + no attributes will be added. + :type attributes: dict[str, Union[str, int, float, bool]] or None + + :returns: None + + .. versionadded:: 2.35.0 + + Example:: + + import sentry_sdk + from sentry_sdk.consts import OP + + sentry_sdk.update_current_span( + op=OP.FUNCTION, + name="process_user_data", + attributes={"user_id": 123, "batch_size": 50} + ) + """ + current_span = get_current_span() + + if current_span is None: + return + + if op is not None: + current_span.op = op + + if name is not None: + current_span.name = name + + if attributes is not None: + current_span.set_attributes(attributes) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 7ec44c0a92..29a5b21434 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -103,6 +103,9 @@ class SPANDATA: AI_CITATIONS = "ai.citations" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + References or sources cited by the AI model in its response. Example: ["Smith et al. 2020", "Jones 2019"] """ @@ -115,65 +118,97 @@ class SPANDATA: AI_DOCUMENTS = "ai.documents" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Documents or content chunks used as context for the AI model. Example: ["doc1.txt", "doc2.pdf"] """ AI_FINISH_REASON = "ai.finish_reason" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_FINISH_REASONS instead. + The reason why the model stopped generating. Example: "length" """ AI_FREQUENCY_PENALTY = "ai.frequency_penalty" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_FREQUENCY_PENALTY instead. + Used to reduce repetitiveness of generated tokens. Example: 0.5 """ AI_FUNCTION_CALL = "ai.function_call" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls """ AI_GENERATION_ID = "ai.generation_id" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_ID instead. + Unique identifier for the completion. Example: "gen_123abc" """ AI_INPUT_MESSAGES = "ai.input_messages" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MESSAGES instead. + The input messages to an LLM call. Example: [{"role": "user", "message": "hello"}] """ AI_LOGIT_BIAS = "ai.logit_bias" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + For an AI model call, the logit bias """ AI_METADATA = "ai.metadata" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Extra metadata passed to an AI pipeline step. Example: {"executed_function": "add_integers"} """ AI_MODEL_ID = "ai.model_id" """ - The unique descriptor of the model being execugted + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_MODEL or GEN_AI_RESPONSE_MODEL instead. + + The unique descriptor of the model being executed. Example: gpt-4 """ AI_PIPELINE_NAME = "ai.pipeline.name" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_PIPELINE_NAME instead. + Name of the AI pipeline or chain being executed. - DEPRECATED: Use GEN_AI_PIPELINE_NAME instead. Example: "qa-pipeline" """ AI_PREAMBLE = "ai.preamble" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + For an AI model call, the preamble parameter. Preambles are a part of the prompt used to adjust the model's overall behavior and conversation style. Example: "You are now a clown." @@ -181,6 +216,9 @@ class SPANDATA: AI_PRESENCE_PENALTY = "ai.presence_penalty" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_PRESENCE_PENALTY instead. + Used to reduce repetitiveness of generated tokens. Example: 0.5 """ @@ -193,89 +231,133 @@ class SPANDATA: AI_RAW_PROMPTING = "ai.raw_prompting" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Minimize pre-processing done to the prompt sent to the LLM. Example: true """ AI_RESPONSE_FORMAT = "ai.response_format" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + For an AI model call, the format of the response """ AI_RESPONSES = "ai.responses" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TEXT instead. + The responses to an AI model call. Always as a list. Example: ["hello", "world"] """ AI_SEARCH_QUERIES = "ai.search_queries" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Queries used to search for relevant context or documents. Example: ["climate change effects", "renewable energy"] """ AI_SEARCH_REQUIRED = "ai.is_search_required" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Boolean indicating if the model needs to perform a search. Example: true """ AI_SEARCH_RESULTS = "ai.search_results" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Results returned from search queries for context. Example: ["Result 1", "Result 2"] """ AI_SEED = "ai.seed" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_SEED instead. + The seed, ideally models given the same seed and same other parameters will produce the exact same output. Example: 123.45 """ AI_STREAMING = "ai.streaming" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_STREAMING instead. + Whether or not the AI model call's response was streamed back asynchronously - DEPRECATED: Use GEN_AI_RESPONSE_STREAMING instead. Example: true """ AI_TAGS = "ai.tags" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Tags that describe an AI pipeline step. Example: {"executed_function": "add_integers"} """ AI_TEMPERATURE = "ai.temperature" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TEMPERATURE instead. + For an AI model call, the temperature parameter. Temperature essentially means how random the output will be. Example: 0.5 """ AI_TEXTS = "ai.texts" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Raw text inputs provided to the model. Example: ["What is machine learning?"] """ AI_TOP_K = "ai.top_k" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_K instead. + For an AI model call, the top_k parameter. Top_k essentially controls how random the output will be. Example: 35 """ AI_TOP_P = "ai.top_p" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_TOP_P instead. + For an AI model call, the top_p parameter. Top_p essentially controls how random the output will be. Example: 0.5 """ AI_TOOL_CALLS = "ai.tool_calls" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_RESPONSE_TOOL_CALLS instead. + For an AI model call, the function that was called. This is deprecated for OpenAI, and replaced by tool_calls """ AI_TOOLS = "ai.tools" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_REQUEST_AVAILABLE_TOOLS instead. + For an AI model call, the functions that are available """ @@ -287,6 +369,9 @@ class SPANDATA: AI_WARNINGS = "ai.warnings" """ + .. deprecated:: + This attribute is deprecated. Use GEN_AI_* attributes instead. + Warning messages generated during model execution. Example: ["Token limit exceeded"] """ @@ -391,6 +476,18 @@ class SPANDATA: Example: "qa-pipeline" """ + GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons" + """ + The reason why the model stopped generating. + Example: "COMPLETE" + """ + + GEN_AI_RESPONSE_ID = "gen_ai.response.id" + """ + Unique identifier for the completion. + Example: "gen_123abc" + """ + GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" """ Exact model identifier used to generate the response @@ -451,12 +548,24 @@ class SPANDATA: Example: 0.1 """ + GEN_AI_REQUEST_SEED = "gen_ai.request.seed" + """ + The seed, ideally models given the same seed and same other parameters will produce the exact same output. + Example: "1234567890" + """ + GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature" """ The temperature parameter used to control randomness in the output. Example: 0.7 """ + GEN_AI_REQUEST_TOP_K = "gen_ai.request.top_k" + """ + Limits the model to only consider the K most likely next tokens, where K is an integer (e.g., top_k=20 means only the 20 highest probability tokens are considered). + Example: 35 + """ + GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p" """ The top_p parameter used to control diversity via nucleus sampling. diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 4cdec031e1..81d8c17b51 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -352,7 +352,8 @@ def name(self) -> Optional[str]: return self.get_attribute(SentrySpanAttribute.NAME) @name.setter - def name(self, value: Optional[str]) -> None: + def name(self, value: str) -> None: + self._otel_span.update_name(value) self.set_attribute(SentrySpanAttribute.NAME, value) @property @@ -450,6 +451,10 @@ def set_attribute(self, key: str, value: Any) -> None: self._otel_span.set_attribute(key, serialized_value) + def set_attributes(self, attributes: dict[str, Any]) -> None: + for key, value in attributes.items(): + self.set_attribute(key, value) + @property def status(self) -> Optional[str]: """ diff --git a/tests/tracing/test_misc.py b/tests/tracing/test_misc.py index 4d85594324..9ef01aa80d 100644 --- a/tests/tracing/test_misc.py +++ b/tests/tracing/test_misc.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, ANY import sentry_sdk from sentry_sdk import start_span, get_current_scope @@ -194,3 +194,87 @@ def test_should_propagate_trace_to_sentry( client.transport.parsed_dsn = Dsn(dsn) assert should_propagate_trace(client, url) == expected_propagation_decision + + +def test_span_set_attributes(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + + events = capture_events() + + with sentry_sdk.start_span(name="test-root-span"): + with start_span(op="test-span", name="test-span-name") as span: + span.set_data("key0", "value0") + span.set_data("key1", "value1") + + span.set_attributes( + { + "key1": "updated-value1", + "key2": "value2", + "key3": "value3", + } + ) + + (event,) = events + span = event["spans"][0] + + assert span["data"] == { + "key0": "value0", + "key1": "updated-value1", + "key2": "value2", + "key3": "value3", + "sentry.name": "test-span-name", + "sentry.op": "test-span", + "sentry.origin": "manual", + "sentry.source": "custom", + "thread.id": ANY, + "thread.name": ANY, + } + + +def test_update_current_span(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + + events = capture_events() + + with sentry_sdk.start_span(name="test-root-span"): + with start_span(op="test-span-op", name="test-span-name"): + sentry_sdk.update_current_span( + op="updated-span-op", + name="updated-span-name", + attributes={ + "key0": "value0", + "key1": "value1", + }, + ) + + sentry_sdk.update_current_span( + op="updated-span-op-2", + ) + + sentry_sdk.update_current_span( + name="updated-span-name-3", + ) + + sentry_sdk.update_current_span( + attributes={ + "key1": "updated-value-4", + "key2": "value2", + }, + ) + + (event,) = events + span = event["spans"][0] + + assert span["op"] == "updated-span-op-2" + assert span["description"] == "updated-span-name-3" + assert span["data"] == { + "key0": "value0", + "key1": "updated-value-4", + "key2": "value2", + "sentry.name": "updated-span-name-3", + "sentry.op": "updated-span-op-2", + "sentry.origin": "manual", + "sentry.source": "custom", + "thread.id": ANY, + "thread.name": ANY, + }