Skip to content
1 change: 0 additions & 1 deletion sentry_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ class DataCollectionUserOptions(TypedDict, total=False):
frame_context_lines: int

class DataCollection(TypedDict):
provided_by_user: bool
user_info: bool
cookies: "KeyValueCollectionBehaviour"
http_headers: "HttpHeadersCollectionBehaviour"
Expand Down
29 changes: 4 additions & 25 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
ClientConstructor,
)
from sentry_sdk.data_collection import (
_map_from_send_default_pii,
_resolve_data_collection,
)
from sentry_sdk.envelope import Envelope, Item
Expand Down Expand Up @@ -153,9 +152,10 @@ class BaseClient:
spotlight: "Optional[SpotlightClient]" = None

def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None:
self.options: "Dict[str, Any]" = (
options if options is not None else DEFAULT_OPTIONS
)
if options is None:
options = dict(DEFAULT_OPTIONS)
options["data_collection"] = _resolve_data_collection(options)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to do this extra step because we need to resolve the empty dictionary into default values.

self.options: "Dict[str, Any]" = options

self.transport: "Optional[Transport]" = None
self.monitor: "Optional[Monitor]" = None
Expand All @@ -178,9 +178,6 @@ def dsn(self) -> "Optional[str]":
def parsed_dsn(self) -> "Optional[Dsn]":
return None

def should_send_default_pii(self) -> bool:
return False

def is_active(self) -> bool:
"""
.. versionadded:: 2.0.0
Expand Down Expand Up @@ -366,18 +363,8 @@ def _record_lost_event(
self.spotlight = setup_spotlight(self.options)
if self.spotlight is not None and not self.options["dsn"]:
sample_all = lambda *_args, **_kwargs: 1.0
self.options["send_default_pii"] = True
self.options["error_sampler"] = sample_all
self.options["traces_sampler"] = sample_all
# data_collection was resolved in _get_options() before this
# spotlight override flipped send_default_pii on. Re-derive it so
# data_collection agrees with should_send_default_pii() in
# DSN-less spotlight mode (only when the user did not set
# data_collection explicitly).
if not self.options["data_collection"]["provided_by_user"]:
self.options["data_collection"] = _map_from_send_default_pii(
send_default_pii=True
)

self.session_flusher = SessionFlusher(capture_func=_capture_envelope)

Expand Down Expand Up @@ -451,14 +438,6 @@ def is_active(self) -> bool:
"""
return True

def should_send_default_pii(self) -> bool:
"""
.. versionadded:: 2.0.0

Returns whether the client should send default PII (Personally Identifiable Information) data to Sentry.
"""
return self.options.get("send_default_pii") or False

@property
def dsn(self) -> "Optional[str]":
"""Returns the configured DSN as string."""
Expand Down
10 changes: 4 additions & 6 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ def __init__(
transport_queue_size: int = DEFAULT_QUEUE_SIZE,
sample_rate: float = 1.0,
send_default_pii: "Optional[bool]" = None,
data_collection: "Optional[DataCollectionUserOptions]" = None,
data_collection: "Optional[DataCollectionUserOptions]" = {},
http_proxy: "Optional[str]" = None,
https_proxy: "Optional[str]" = None,
ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006
Expand Down Expand Up @@ -1505,12 +1505,10 @@ def __init__(
managing `Sensitive Data <https://docs.sentry.io/data-management/sensitive-data/>`_.

:param data_collection: Structured configuration controlling what data integrations collect
automatically, superseding `send_default_pii`. Passing a dict opts into the feature; omitted
fields use their defaults (most categories are collected, with the sensitive denylist
scrubbing values). When it is not set, the SDK derives behaviour from `send_default_pii` so
that upgrading changes nothing. Restrict collection per category (user identity, cookies,
automatically. Omitted fields use their defaults (most categories are collected, with the sensitive denylist
scrubbing values). Restrict collection per category (user identity, cookies,
HTTP headers/bodies, query params, generative AI inputs/outputs, stack frame variables,
source context). If `send_default_pii` is also set, `data_collection` takes precedence.
source context).

Example::

Expand Down
79 changes: 10 additions & 69 deletions sentry_sdk/data_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,10 @@
"Data Collection" spec
(https://develop.sentry.dev/sdk/foundations/client/data-collection/).

``data_collection`` supersedes the single ``send_default_pii`` boolean with a
structured configuration that lets users enable or restrict automatically
``data_collection`` lets users enable or restrict automatically
collected data by category (user identity, cookies, HTTP headers, URL query params,
HTTP bodies, generative AI inputs/outputs, stack frame variables, source
context).

Resolution precedence (see :func:`_resolve_data_collection`):

* ``data_collection`` set, ``send_default_pii`` unset -> honour ``data_collection``
using the spec defaults for any omitted field.
* ``send_default_pii`` set, ``data_collection`` unset -> derive a
resolved ``DataCollection`` that mirrors what ``send_default_pii`` collects today.
* neither set -> treated as ``send_default_pii=False``.
* both set -> ``data_collection`` wins (it is the single source of truth); a
``DeprecationWarning`` is emitted for ``send_default_pii``.
"""

from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Union, cast
Expand Down Expand Up @@ -149,37 +138,7 @@ def _apply_key_value_collection_filtering(
return result


def _map_from_send_default_pii(*, send_default_pii: bool) -> "DataCollection":
"""
Build a fully-resolved ``DataCollection`` dict that mirrors the data
``send_default_pii`` collects today. Used when ``data_collection`` is not
provided explicitly.
"""
terms = [] if send_default_pii else ["forwarded", "-ip", "remote-", "via", "-user"]

return {
"provided_by_user": False,
"user_info": send_default_pii,
"cookies": {"mode": "denylist", "terms": terms},
# Headers are collected in both PII modes today (sensitive ones filtered
# when PII is off), so this never maps to "off".
"http_headers": {
"request": {"mode": "denylist", "terms": terms},
},
# Bodies are collected regardless of PII today, bounded by
# ``max_request_body_size``.
"http_bodies": list(_ALL_HTTP_BODY_TYPES),
"url_query_params": {"mode": "denylist", "terms": terms},
"graphql": {"document": send_default_pii, "variables": send_default_pii},
"gen_ai": {"inputs": send_default_pii, "outputs": send_default_pii},
"database_query_data": send_default_pii,
"queues": send_default_pii,
"stack_frame_variables": True,
"frame_context_lines": _DEFAULT_FRAME_CONTEXT_LINES,
}


def _resolve_explicit(
def _resolve(
d: "dict[str, Any]",
) -> "DataCollection":
"""
Expand Down Expand Up @@ -214,7 +173,6 @@ def _resolve_explicit(
)

return {
"provided_by_user": True,
"user_info": d.get("user_info", True),
"cookies": _kvcb_from_value(d.get("cookies") or {}),
"http_headers": _http_headers_from_value(d.get("http_headers") or {}),
Expand Down Expand Up @@ -280,37 +238,20 @@ def _resolve_data_collection(options: "Dict[str, Any]") -> "DataCollection":
"""
Resolve the effective ``DataCollection`` dict from client ``options``.

Reads ``data_collection``, ``send_default_pii`` and returns a fully-resolved
dict with concrete values for every field.
Reads ``data_collection`` and returns a fully-resolved dict with concrete values for every field.

``data_collection`` must be a plain ``dict``.

Must be called exactly once per options dict, before ``client._get_options``
overwrites ``options["data_collection"]`` with the resolved result. Feeding an
already-resolved dict back in would flip ``provided_by_user`` to ``True``.
overwrites ``options["data_collection"]`` with the resolved result.
"""
from sentry_sdk.utils import deprecation_warning

user_dc = options.get("data_collection")
if user_dc is None:
user_dc = options.get("_experiments", {}).get("data_collection")

send_default_pii = options.get("send_default_pii")

if user_dc is not None:
if not isinstance(user_dc, dict):
raise TypeError(
"`data_collection` must be a dict, got {!r}.".format(
type(user_dc).__name__
)
)
if send_default_pii is not None:
deprecation_warning(
"`send_default_pii` is deprecated and ignored when "
"`data_collection` is set.",
)
return _resolve_explicit(
user_dc,
if not isinstance(user_dc, dict):
raise TypeError(
"`data_collection` must be a dict, got {!r}.".format(type(user_dc).__name__)
Comment thread
cursor[bot] marked this conversation as resolved.
)

return _map_from_send_default_pii(send_default_pii=bool(send_default_pii))
return _resolve(
user_dc,
)
20 changes: 7 additions & 13 deletions sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
get_current_span,
)
from sentry_sdk.utils import (
has_data_collection_enabled,
parse_version,
)

Expand Down Expand Up @@ -116,12 +115,10 @@ def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
if "cookies" in info:
request_info["cookies"] = info["cookies"]
if "data" in info:
attach_request_data = True
if has_data_collection_enabled(client.options):
attach_request_data = (
"incoming_request"
in client.options["data_collection"]["http_bodies"]
)
attach_request_data = (
"incoming_request"
in client.options["data_collection"]["http_bodies"]
)

if attach_request_data:
request_info["data"] = info["data"]
Expand All @@ -142,12 +139,9 @@ def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
current_span = get_current_span()

if type(current_span) is Span:
attach_request_data = True
if has_data_collection_enabled(client.options):
attach_request_data = (
"incoming_request"
in client.options["data_collection"]["http_bodies"]
)
attach_request_data = (
"incoming_request" in client.options["data_collection"]["http_bodies"]
)

if attach_request_data:
request_body = _get_cached_request_body_attribute(
Expand Down
7 changes: 5 additions & 2 deletions sentry_sdk/integrations/strawberry.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ def on_operation(self) -> "Generator[None, None, None]":
return

additional_attributes: "dict[str, Any]" = {}

if client.options["data_collection"]["graphql"]["document"]:
additional_attributes["graphql.document"] = self.execution_context.query

Expand Down Expand Up @@ -395,7 +394,11 @@ def _make_response_event_processor(
def inner(event: "Event", hint: "dict[str, Any]") -> "Event":
client_options = sentry_sdk.get_client().options
with capture_internal_exceptions():
if "outgoing_response" in client_options["data_collection"]["http_bodies"]:
collect_response = (
"outgoing_response" in client_options["data_collection"]["http_bodies"]
)

if collect_response:
contexts = event.setdefault("contexts", {})
contexts["response"] = {"data": response_data}

Expand Down
11 changes: 1 addition & 10 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
event_from_exception,
exc_info_from_error,
format_attribute,
has_data_collection_enabled,
logger,
)

Expand Down Expand Up @@ -1443,10 +1442,7 @@ def _apply_user_attributes_to_telemetry(
attributes = telemetry._attributes

client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if not client_options["data_collection"]["user_info"] or self._user is None:
return
elif not should_send_default_pii() or self._user is None:
if not client_options["data_collection"]["user_info"] or self._user is None:
return

for attribute_name, user_attribute in (
Expand Down Expand Up @@ -1874,11 +1870,6 @@ def use_isolation_scope(isolation_scope: "Scope") -> "Generator[Scope, None, Non
capture_internal_exception(sys.exc_info())


def should_send_default_pii() -> bool:
"""Shortcut for `Scope.get_client().should_send_default_pii()`."""
return Scope.get_client().should_send_default_pii()


# Circular imports
from sentry_sdk.client import NonRecordingClient

Expand Down
23 changes: 7 additions & 16 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
_module_in_list,
capture_internal_exceptions,
filename_for_module,
has_data_collection_enabled,
is_sentry_url,
is_valid_sample_rate,
logger,
Expand Down Expand Up @@ -185,26 +184,18 @@ def get_url_attributes_legacy(
) -> "Attributes":
"""Build the `url.*` span attributes for an outgoing HTTP request.

The query string is only included when the user has opted into collecting
it, either through `data_collection` (in which case the configured
filtering is applied) or through the legacy `send_default_pii`.
The query string is filtered according to `data_collection`.
"""
attributes: "Attributes" = {}
if parsed_url is None:
return attributes

query: "Optional[str]"
if has_data_collection_enabled(client.options):
query = None
if parsed_url.query:
query = _apply_data_collection_filtering_to_query_string(
query_string=parsed_url.query,
behaviour=client.options["data_collection"]["url_query_params"],
)
elif client.should_send_default_pii():
query = parsed_url.query
else:
return attributes
query: "Optional[str]" = None
if parsed_url.query:
query = _apply_data_collection_filtering_to_query_string(
query_string=parsed_url.query,
behaviour=client.options["data_collection"]["url_query_params"],
)

url_full = parsed_url.url
if query:
Expand Down
20 changes: 1 addition & 19 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@

from sentry_sdk._types import (
AttributeValue,
DataCollection,
Event,
ExcInfo,
Hint,
Expand Down Expand Up @@ -477,11 +476,7 @@ def get_lines_from_file(
) -> "Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]":
client_options = sentry_sdk.get_client().options

# This is the default pre-data collection. Should be removed once data collection
# is fully released
context_lines = 5
if has_data_collection_enabled(client_options):
context_lines = client_options["data_collection"]["frame_context_lines"]
context_lines = client_options["data_collection"]["frame_context_lines"]

source = None
if loader is not None and hasattr(loader, "get_source"):
Expand Down Expand Up @@ -1910,19 +1905,6 @@ def serialize_item(
return str(data)


def has_data_collection_enabled(options: "Optional[dict[str, Any]]") -> bool:
if options is None:
return False

data_collection: "Optional[DataCollection]" = options.get("data_collection")
# Client options are resolved as part of client initialization, so `data_collection`
# being None could be that the user just didn't provide it.
# `provided_by_user` is what actually records whether the user actually configured it.
return data_collection is not None and data_collection.get(
"provided_by_user", False
)


def get_before_send_log(
options: "Optional[dict[str, Any]]",
) -> "Optional[Callable[[Log, Hint], Optional[Log]]]":
Expand Down
Loading
Loading