From 0bb8204f9248c6ccd999d9dbf0e3298d7e13343a Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 14 Oct 2026 18:45:03 +0200 Subject: [PATCH 1/4] ref(boto3): Use `rpc` as the default span op --- sentry_sdk/consts.py | 1 + .../integrations/boto3/_instrumentation.py | 2 +- tests/integrations/boto3/test_client.py | 32 +++++++++---------- tests/integrations/boto3/test_dynamodb.py | 2 +- tests/integrations/boto3/test_kinesis.py | 2 +- tests/integrations/boto3/test_lambda.py | 2 +- tests/integrations/boto3/test_s3.py | 4 +-- .../integrations/boto3/test_secretsmanager.py | 4 +-- tests/integrations/boto3/test_sns.py | 2 +- tests/integrations/boto3/test_sqs.py | 2 +- .../integrations/boto3/test_stepfunctions.py | 2 +- 11 files changed, 28 insertions(+), 27 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index abf1464b68..7ab271473e 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1389,6 +1389,7 @@ class OP: QUEUE_TASK_RAY = "queue.task.ray" QUEUE_TASK_DRAMATIQ = "queue.task.dramatiq" QUEUE_SUBMIT_DJANGO = "queue.submit.django" + RPC = "rpc" SUBPROCESS = "subprocess" SUBPROCESS_WAIT = "subprocess.wait" SUBPROCESS_COMMUNICATE = "subprocess.communicate" diff --git a/sentry_sdk/integrations/boto3/_instrumentation.py b/sentry_sdk/integrations/boto3/_instrumentation.py index e3ee4fd7d6..fab0122ccb 100644 --- a/sentry_sdk/integrations/boto3/_instrumentation.py +++ b/sentry_sdk/integrations/boto3/_instrumentation.py @@ -169,7 +169,7 @@ def _start_client_span( # `sentry.op` and `sentry.origin` are set to generic defaults; # a service extension can override them with `get_span_op()` and `get_span_origin()`. - span_op = OP.HTTP_CLIENT + span_op = OP.RPC span_origin = ORIGIN if service_ext is not None: diff --git a/tests/integrations/boto3/test_client.py b/tests/integrations/boto3/test_client.py index 087a6ec7df..163fc515bc 100644 --- a/tests/integrations/boto3/test_client.py +++ b/tests/integrations/boto3/test_client.py @@ -146,7 +146,7 @@ def record_client_span(request, **kwargs): if span["name"] == "S3.GetObject" and ( span.get("attributes", {}).get(SPANDATA.SENTRY_ORIGIN) == ORIGIN - and span.get("attributes", {}).get(SPANDATA.SENTRY_OP) == OP.HTTP_CLIENT + and span.get("attributes", {}).get(SPANDATA.SENTRY_OP) == OP.RPC ) ] http_spans = [ @@ -231,7 +231,7 @@ def invoke(): assert sentry_sdk.get_current_span() is parent spans_by_op = _capture_boto3_spans_by_op(invoke, capture_items) - (client_span,) = spans_by_op[OP.HTTP_CLIENT] + (client_span,) = spans_by_op[OP.RPC] (stream_span,) = spans_by_op[OP.HTTP_CLIENT_STREAM] assert stream_span.get("parent_span_id") == client_span["span_id"] @@ -251,7 +251,7 @@ def test_omit_url_data_if_parsing_fails(capture_items, client_factory): ) parse_url.assert_called() - (span,) = spans_by_op[OP.HTTP_CLIENT] + (span,) = spans_by_op[OP.RPC] attributes = span.get("attributes", {}) assert SPANDATA.URL_FULL not in attributes assert SPANDATA.URL_FRAGMENT not in attributes @@ -326,7 +326,7 @@ def test_url_query_data_collection( (span,) = [ item.payload for item in items - if item.payload.get("attributes", {}).get(SPANDATA.SENTRY_OP) == OP.HTTP_CLIENT + if item.payload.get("attributes", {}).get(SPANDATA.SENTRY_OP) == OP.RPC ] attributes = span.get("attributes", {}) @@ -428,7 +428,7 @@ def _capture_stubbed_client_span( ) stubber.assert_no_pending_responses() - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) assert len(client_spans) == 1 return client_spans[0] @@ -580,7 +580,7 @@ def test_client_call_has_common_attributes( assert span["name"] == span_name assert span.get("end_timestamp") is not None - assert attributes[SPANDATA.SENTRY_OP] == OP.HTTP_CLIENT + assert attributes[SPANDATA.SENTRY_OP] == OP.RPC assert attributes[SPANDATA.SENTRY_ORIGIN] == ORIGIN assert attributes[SPANDATA.SENTRY_KIND] == "client" assert attributes[SPANDATA.CLOUD_PROVIDER] == CLOUD_PROVIDER @@ -651,7 +651,7 @@ def test_client_call_has_response_header_attributes( capture_items, ) - spans = spans_by_op[OP.HTTP_CLIENT] + spans = spans_by_op[OP.RPC] assert len(spans) == 1 attributes = spans[0].get("attributes", {}) assert attributes[SPANDATA.HTTP_STATUS_CODE] == 200 @@ -670,7 +670,7 @@ def test_retry_attempts_share_one_client_span( spans_by_op = _capture_boto3_spans_by_op( lambda: client.head_object(Bucket="bucket", Key="foo"), capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) assert len(request_span_ids) == attempt_count # all `AWSRequest` instances created during retries reference the same client span. @@ -693,7 +693,7 @@ def attempt_failed_head_object_call(): spans_by_op = _capture_boto3_spans_by_op( attempt_failed_head_object_call, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) assert len(request_span_ids) == 2 assert len(set(request_span_ids)) == 1 @@ -749,14 +749,14 @@ def invoke_failing_client_method(): spans_by_op = _capture_boto3_spans_by_op( invoke_failing_client_method, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) _assert_one_failed_span(client_spans) attributes = client_spans[0].get("attributes", {}) span = client_spans[0] assert span["name"] == "S3.HeadObject" assert span.get("end_timestamp") is not None - assert attributes[SPANDATA.SENTRY_OP] == OP.HTTP_CLIENT + assert attributes[SPANDATA.SENTRY_OP] == OP.RPC assert attributes[SPANDATA.SENTRY_ORIGIN] == ORIGIN assert attributes[SPANDATA.SENTRY_KIND] == "client" assert attributes[SPANDATA.CLOUD_PROVIDER] == CLOUD_PROVIDER @@ -810,7 +810,7 @@ def invoke_failing_client_method(): spans_by_op = _capture_boto3_spans_by_op( invoke_failing_client_method, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) _assert_one_failed_span(client_spans) expected_error_type = ( @@ -856,7 +856,7 @@ def invoke_client_method(): stubber.add_response("head_object", original_response, api_params) spans_by_op = _capture_boto3_spans_by_op(invoke_client_method, capture_items) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) assert returned_responses == [original_response] assert returned_responses[0] is original_response if failing_instrumentation == "_get_response_attributes": @@ -893,7 +893,7 @@ def invoke_failing_client_method(): spans_by_op = _capture_boto3_spans_by_op( invoke_failing_client_method, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) assert len(client_spans) == 1 assert client_spans[0]["status"] == "error" @@ -926,7 +926,7 @@ def invoke_client_method_and_read_body(): spans_by_op = _capture_boto3_spans_by_op( invoke_client_method_and_read_body, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) stream_spans = spans_by_op.get(OP.HTTP_CLIENT_STREAM, []) assert len(client_spans) == 1 @@ -975,7 +975,7 @@ def invoke_client_method_and_read_body(): spans_by_op = _capture_boto3_spans_by_op( invoke_client_method_and_read_body, capture_items ) - client_spans = spans_by_op.get(OP.HTTP_CLIENT, []) + client_spans = spans_by_op.get(OP.RPC, []) stream_spans = spans_by_op.get(OP.HTTP_CLIENT_STREAM, []) assert len(client_spans) == 1 diff --git a/tests/integrations/boto3/test_dynamodb.py b/tests/integrations/boto3/test_dynamodb.py index 339628099c..5399e035ae 100644 --- a/tests/integrations/boto3/test_dynamodb.py +++ b/tests/integrations/boto3/test_dynamodb.py @@ -36,7 +36,7 @@ def test_request_attributes( lambda: getattr(client, method)(**params), capture_items ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] if expected_resource_id is None: assert SPANDATA.CLOUD_ACCOUNT_ID not in attributes diff --git a/tests/integrations/boto3/test_kinesis.py b/tests/integrations/boto3/test_kinesis.py index c3911b078d..c643d62595 100644 --- a/tests/integrations/boto3/test_kinesis.py +++ b/tests/integrations/boto3/test_kinesis.py @@ -18,7 +18,7 @@ def test_request_attributes(client_factory, capture_items): capture_items, ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == stream_arn diff --git a/tests/integrations/boto3/test_lambda.py b/tests/integrations/boto3/test_lambda.py index d74818fd3b..00c5d58df1 100644 --- a/tests/integrations/boto3/test_lambda.py +++ b/tests/integrations/boto3/test_lambda.py @@ -16,7 +16,7 @@ def test_request_attributes(client_factory, capture_items): capture_items, ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == function_arn diff --git a/tests/integrations/boto3/test_s3.py b/tests/integrations/boto3/test_s3.py index c21dfbb713..0e1ef6a6e5 100644 --- a/tests/integrations/boto3/test_s3.py +++ b/tests/integrations/boto3/test_s3.py @@ -26,12 +26,12 @@ def _stubbed_span(client, capture_items, method, params, response): lambda: getattr(client, method)(**params), capture_items ) stubber.assert_no_pending_responses() - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] operation = client.meta.method_to_api_mapping[method] assert span["name"] == "S3.%s" % operation assert span.get("end_timestamp") is not None attributes = span.get("attributes", {}) - assert attributes[SPANDATA.SENTRY_OP] == OP.HTTP_CLIENT + assert attributes[SPANDATA.SENTRY_OP] == OP.RPC assert attributes[SPANDATA.SENTRY_ORIGIN] == ORIGIN assert attributes[SPANDATA.SENTRY_KIND] == "client" assert attributes[SPANDATA.CLOUD_PROVIDER] == "aws" diff --git a/tests/integrations/boto3/test_secretsmanager.py b/tests/integrations/boto3/test_secretsmanager.py index f44f25515a..6ec3875c0d 100644 --- a/tests/integrations/boto3/test_secretsmanager.py +++ b/tests/integrations/boto3/test_secretsmanager.py @@ -22,7 +22,7 @@ def test_request_attributes(client_factory, capture_items): capture_items, ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" - assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == secret_arn + assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == secret_arn \ No newline at end of file diff --git a/tests/integrations/boto3/test_sns.py b/tests/integrations/boto3/test_sns.py index 7e613e919e..7f593d357e 100644 --- a/tests/integrations/boto3/test_sns.py +++ b/tests/integrations/boto3/test_sns.py @@ -15,7 +15,7 @@ def test_request_attributes(client_factory, capture_items): lambda: client.delete_topic(**{"TopicArn": topic_arn}), capture_items ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == topic_arn diff --git a/tests/integrations/boto3/test_sqs.py b/tests/integrations/boto3/test_sqs.py index b05d40de91..ecc81c69d9 100644 --- a/tests/integrations/boto3/test_sqs.py +++ b/tests/integrations/boto3/test_sqs.py @@ -33,7 +33,7 @@ def test_request_attributes(client_factory, capture_items, source): lambda: getattr(client, method)(**params), capture_items ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" if resource_id is None: diff --git a/tests/integrations/boto3/test_stepfunctions.py b/tests/integrations/boto3/test_stepfunctions.py index 52de542eb3..e080effc46 100644 --- a/tests/integrations/boto3/test_stepfunctions.py +++ b/tests/integrations/boto3/test_stepfunctions.py @@ -20,7 +20,7 @@ def test_request_attributes(client_factory, capture_items): capture_items, ) - (span,) = spans[OP.HTTP_CLIENT] + (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == state_machine_arn From c153076aac184375eb5a087be68f17013a5623cc Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 14 Oct 2026 18:51:41 +0200 Subject: [PATCH 2/4] lint --- tests/integrations/boto3/test_secretsmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/boto3/test_secretsmanager.py b/tests/integrations/boto3/test_secretsmanager.py index 6ec3875c0d..da0a844e13 100644 --- a/tests/integrations/boto3/test_secretsmanager.py +++ b/tests/integrations/boto3/test_secretsmanager.py @@ -25,4 +25,4 @@ def test_request_attributes(client_factory, capture_items): (span,) = spans[OP.RPC] attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" - assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == secret_arn \ No newline at end of file + assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == secret_arn From 0c31a6d711dbb17335e9fee32e909c2cd4a9ff99 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 15 Oct 2026 15:39:44 +0200 Subject: [PATCH 3/4] migration guide --- MIGRATION_GUIDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 335d9025d7..b6cd583dd8 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -39,6 +39,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - In the AWS Lambda and GCP integrations, the text of the message of the warning the SDK optionally emits if a function is about to time out has changed. - The `http.request.header.` span attributes set by server integrations are now lists of strings instead of strings. - The HTTP breadcrumbs created by the stdlib (`http.client`) and boto3 integrations now use different keys: `http.method` is now `http.request.method`, `url` (or `aws.request.url` for boto3) is now `url.full`, `http.query` is now `url.query`, and `http.fragment` is now `url.fragment`. The stdlib breadcrumb no longer contains `reason`. The query string is filtered according to `data_collection`'s `url_query_params` setting. + - The `url` field of HTTP breadcrumbs created by the HTTPX, httpx2, pyreqwest, and AIOHTTP client integrations now contains the full URL, including the query string and fragment. The query string is filtered according to `data_collection`'s `url_query_params` setting. - The breadcrumbs created by the PyMongo integration now use different keys: `db.name` is now `db.namespace`, `db.system` is now `db.system.name`, `db.operation` is now `db.operation.name`, `db.mongodb.collection` is now `db.collection.name`, and `net.peer.name` and `net.peer.port` are now `server.address` and `server.port`. - The connection breadcrumbs created by the asyncpg and aiomysql integrations now use `db.system.name` instead of `db.system` and `db.namespace` instead of `db.name`. @@ -63,6 +64,7 @@ Span ops are now a smaller, framework-neutral set. - The Django integration now emits `view.render` instead of `view.response.render`, `view` instead of `view.authenticate`, and `function` instead of `event.django`. - The Django, Starlette, Starlite, and Litestar integrations now emit `middleware` instead of `middleware.django`, `middleware.starlette`, `middleware.starlette.receive`, `middleware.starlette.send`, `middleware.starlite`, `middleware.starlite.receive`, `middleware.starlite.send`, `middleware.litestar`, `middleware.litestar.receive`, and `middleware.litestar.send`. - The ARQ, Celery, Django, Dramatiq, Huey, RQ, and Ray integrations now emit `queue.publish` and `queue.process` instead of `queue.submit.arq`, `queue.task.arq`, `queue.submit.celery`, `queue.task.celery`, `queue.submit.django`, `queue.task.dramatiq`, `queue.submit.huey`, `queue.task.huey`, `queue.task.rq`, `queue.submit.ray`, and `queue.task.ray`. +- The Boto3 integration now emits `rpc` instead of `http.client` for AWS API calls. #### GenAI span attributes From 4a4bf08b14c3350e9767a9552887e03e5a437fd0 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Thu, 15 Oct 2026 15:40:37 +0200 Subject: [PATCH 4/4] remove newline --- MIGRATION_GUIDE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index b6cd583dd8..6b10de08d6 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -39,7 +39,6 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - In the AWS Lambda and GCP integrations, the text of the message of the warning the SDK optionally emits if a function is about to time out has changed. - The `http.request.header.` span attributes set by server integrations are now lists of strings instead of strings. - The HTTP breadcrumbs created by the stdlib (`http.client`) and boto3 integrations now use different keys: `http.method` is now `http.request.method`, `url` (or `aws.request.url` for boto3) is now `url.full`, `http.query` is now `url.query`, and `http.fragment` is now `url.fragment`. The stdlib breadcrumb no longer contains `reason`. The query string is filtered according to `data_collection`'s `url_query_params` setting. - - The `url` field of HTTP breadcrumbs created by the HTTPX, httpx2, pyreqwest, and AIOHTTP client integrations now contains the full URL, including the query string and fragment. The query string is filtered according to `data_collection`'s `url_query_params` setting. - The breadcrumbs created by the PyMongo integration now use different keys: `db.name` is now `db.namespace`, `db.system` is now `db.system.name`, `db.operation` is now `db.operation.name`, `db.mongodb.collection` is now `db.collection.name`, and `net.peer.name` and `net.peer.port` are now `server.address` and `server.port`. - The connection breadcrumbs created by the asyncpg and aiomysql integrations now use `db.system.name` instead of `db.system` and `db.namespace` instead of `db.name`.