From 99de60fc186797afc8fb5f21144b1a95dc3c3d38 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Mon, 12 Oct 2026 18:21:39 +0200 Subject: [PATCH] feat(boto3): Gate user identifying resource id behind `user_info` --- .../integrations/boto3/_services/sns.py | 20 ++++- tests/integrations/boto3/test_sns.py | 86 +++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/boto3/_services/sns.py b/sentry_sdk/integrations/boto3/_services/sns.py index 4c72d64a2d..2ab0610e79 100644 --- a/sentry_sdk/integrations/boto3/_services/sns.py +++ b/sentry_sdk/integrations/boto3/_services/sns.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING +import sentry_sdk from sentry_sdk.integrations.boto3._services.base import _ServiceExtension from sentry_sdk.integrations.boto3._utils import _get_aws_arn_attributes from sentry_sdk.utils import capture_internal_exceptions @@ -13,13 +14,26 @@ class _SNSExtension(_ServiceExtension): def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": attributes: "Attributes" = {} with capture_internal_exceptions(): + endpoint_arn = ctx.params.get("EndpointArn") + subscription_arn = ctx.params.get("SubscriptionArn") + target_arn = ctx.params.get("TargetArn") + + # platform endpoint and subscription ARNs identify a single device or + # subscriber, so gate them with `user_info`; endpoint can be a device + # token, target ARN can be an endpoint, and subscriber email/phone number. + if not sentry_sdk.get_client().options["data_collection"]["user_info"]: + endpoint_arn = None + subscription_arn = None + if target_arn and ":endpoint/" in target_arn: + target_arn = None + attributes.update( _get_aws_arn_attributes( ( - ctx.params.get("EndpointArn"), + endpoint_arn, ctx.params.get("PlatformApplicationArn"), - ctx.params.get("SubscriptionArn"), - ctx.params.get("TargetArn"), + subscription_arn, + target_arn, ctx.params.get("TopicArn"), ), ("sns",), diff --git a/tests/integrations/boto3/test_sns.py b/tests/integrations/boto3/test_sns.py index 7e613e919e..aa81a2f758 100644 --- a/tests/integrations/boto3/test_sns.py +++ b/tests/integrations/boto3/test_sns.py @@ -1,8 +1,11 @@ +import pytest from botocore.stub import Stubber from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.boto3 import Boto3Integration from tests.integrations.boto3.helpers import capture_spans_by_op from tests.integrations.boto3.helpers import client_factory as client_factory +from tests.integrations.utils import DATA_COLLECTION_USER_INFO_CASES def test_request_attributes(client_factory, capture_items): @@ -19,3 +22,86 @@ def test_request_attributes(client_factory, capture_items): attributes = span["attributes"] assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == topic_arn + + +ENDPOINT_ARN = "arn:aws:sns:eu-north-1:123456789012:endpoint/GCM/myapp/5f1c8a3e-0b7d-3c55-9b5e-1a2b3c4d5e6f" + + +@pytest.mark.parametrize( + "method,params,response,user_identifying", + [ + # `EndpointARN` can be a device token. + pytest.param( + "get_endpoint_attributes", + {"EndpointArn": ENDPOINT_ARN}, + {"Attributes": {}}, + True, + id="endpoint_arn", + ), + # `SubscriptionArn` can be a subscriber email/phone number. + pytest.param( + "unsubscribe", + { + "SubscriptionArn": "arn:aws:sns:eu-north-1:123456789012:orders:8e2a6b1c-4d3f-4f6a-9c1e-2b3d4e5f6a7b" + }, + {}, + True, + id="subscription_arn", + ), + # `TargetArn` can be an endpoint ARN. + pytest.param( + "publish", + {"TargetArn": ENDPOINT_ARN, "Message": "hi"}, + {"MessageId": "1"}, + True, + id="target_arn_endpoint", + ), + # `TargetArn` can be a topic ARN, which is not gated. + pytest.param( + "publish", + { + "TargetArn": "arn:aws:sns:eu-north-1:123456789012:orders", + "Message": "hi", + }, + {"MessageId": "1"}, + False, + id="target_arn_topic", + ), + ], +) +@pytest.mark.parametrize( + "data_collection,expect_user_info", DATA_COLLECTION_USER_INFO_CASES +) +def test_resource_id_span_attributes_data_collection( + sentry_init, + client_factory, + capture_items, + method, + params, + response, + user_identifying, + data_collection, + expect_user_info, +): + sentry_init( + traces_sample_rate=1.0, + integrations=[Boto3Integration()], + data_collection=data_collection, + ) + client = client_factory("sns") + (arn,) = (value for key, value in params.items() if key.endswith("Arn")) + + with Stubber(client) as stubber: + stubber.add_response(method, response, params) + spans = capture_spans_by_op( + lambda: getattr(client, method)(**params), capture_items + ) + + (span,) = spans[OP.HTTP_CLIENT] + attributes = span["attributes"] + if expect_user_info or not user_identifying: + assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == arn + assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" + else: + assert SPANDATA.CLOUD_RESOURCE_ID not in attributes + assert SPANDATA.CLOUD_ACCOUNT_ID not in attributes