Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ Self-hosted Sentry users should opt out with `stream_gen_ai_spans=False`, since

- (test): exclude django 6.1 alphas and betas from tox by @ericapisani in [#6690](https://github.com/getsentry/sentry-python/pull/6690)
- (test): need to include the alpha tag for mcp package inclusion by @ericapisani in [#6688](https://github.com/getsentry/sentry-python/pull/6688)

## 2.63.0

### Bug Fixes 🐛
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ sentry_sdk.init(
# Set traces_sample_rate to 1.0 to capture 100%
# of traces for performance monitoring.
traces_sample_rate=1.0,

# To disable sending user data and HTTP request/response bodies, uncomment
# the line below. For more info visit:
# https://docs.sentry.io/platforms/python/configuration/options/#data_collection
# data_collection={"user_info": False, "http_bodies": []},
)
```

Expand Down
61 changes: 60 additions & 1 deletion sentry_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def substituted_because_contains_sensitive_data(cls) -> "AnnotatedValue":
from collections.abc import Container, MutableMapping, Sequence
from datetime import datetime
from types import TracebackType
from typing import Any, Callable, Dict, Mapping, NotRequired, Optional, Type
from typing import Any, Callable, Dict, List, Mapping, NotRequired, Optional, Type

from typing_extensions import Literal, TypedDict

Expand All @@ -152,6 +152,65 @@ class SDKInfo(TypedDict):
version: str
packages: "Sequence[Mapping[str, str]]"

class KeyValueCollectionBehaviour(TypedDict):
mode: 'Literal["off", "deny_list", "allow_list"]'
terms: "NotRequired[List[str]]"

class GenAICollectionUserOptions(TypedDict, total=False):
inputs: bool
outputs: bool

class GenAICollectionBehaviour(TypedDict):
inputs: bool
outputs: bool

class GraphQLCollectionUserOptions(TypedDict, total=False):
document: bool
variables: bool

class GraphQLCollectionBehaviour(TypedDict):
document: bool
variables: bool

class DatabaseCollectionUserOptions(TypedDict, total=False):
query_params: bool

class DatabaseCollectionBehaviour(TypedDict):
query_params: bool

class HttpHeadersCollectionUserOptions(TypedDict, total=False):
request: "KeyValueCollectionBehaviour"
response: "KeyValueCollectionBehaviour"

class HttpHeadersCollectionBehaviour(TypedDict):
request: "KeyValueCollectionBehaviour"
response: "KeyValueCollectionBehaviour"

class DataCollectionUserOptions(TypedDict, total=False):
user_info: bool
cookies: "KeyValueCollectionBehaviour"
http_headers: "HttpHeadersCollectionUserOptions"
http_bodies: "List[str]"
query_params: "KeyValueCollectionBehaviour"
graphql: "GraphQLCollectionUserOptions"
gen_ai: "GenAICollectionUserOptions"
database: "DatabaseCollectionUserOptions"
stack_frame_variables: bool
frame_context_lines: int

class DataCollection(TypedDict):
provided_by_user: bool
user_info: bool
cookies: "KeyValueCollectionBehaviour"
http_headers: "HttpHeadersCollectionBehaviour"
http_bodies: "List[str]"
query_params: "KeyValueCollectionBehaviour"
graphql: "GraphQLCollectionBehaviour"
gen_ai: "GenAICollectionBehaviour"
database: "DatabaseCollectionBehaviour"
stack_frame_variables: bool
frame_context_lines: int

# "critical" is an alias of "fatal" recognized by Relay
LogLevelStr = Literal["fatal", "critical", "error", "warning", "info", "debug"]

Expand Down
42 changes: 39 additions & 3 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
VERSION,
ClientConstructor,
)
from sentry_sdk.data_collection import (
Comment thread
sentry-warden[bot] marked this conversation as resolved.
_map_from_send_default_pii,
resolve_data_collection,
)
from sentry_sdk.envelope import Envelope, Item, PayloadRef
from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations
from sentry_sdk.integrations.dedupe import DedupeIntegration
Expand Down Expand Up @@ -70,6 +74,7 @@
from sentry_sdk._log_batcher import LogBatcher
from sentry_sdk._metrics_batcher import MetricsBatcher
from sentry_sdk._types import (
DataCollection,
Event,
EventDataCategory,
Hint,
Expand Down Expand Up @@ -345,11 +350,11 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]":
if rv["enable_tracing"] is True and rv["traces_sample_rate"] is None:
rv["traces_sample_rate"] = 1.0

rv["data_collection"] = resolve_data_collection(rv)

if rv["event_scrubber"] is None:
rv["event_scrubber"] = EventScrubber(
send_default_pii=(
False if rv["send_default_pii"] is None else rv["send_default_pii"]
)
send_default_pii=rv["data_collection"]["user_info"]
)

if rv["socket_options"] and not isinstance(rv["socket_options"], list):
Expand Down Expand Up @@ -386,6 +391,12 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]":
# Older Python versions
module_not_found_error = ImportError # type: ignore

_DISABLED_DATA_COLLECTION_CONFIG = _map_from_send_default_pii(
send_default_pii=False,
include_local_variables=True,
include_source_context=True,
)


class BaseClient:
"""
Expand Down Expand Up @@ -425,6 +436,10 @@ def parsed_dsn(self) -> "Optional[Dsn]":
def should_send_default_pii(self) -> bool:
return False

@property
def data_collection(self) -> "DataCollection":
return _DISABLED_DATA_COLLECTION_CONFIG

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to add this accessor?

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.

We might not depending on our convo in the other comment that's related.

This was added for consistency with dsn and should_send_default_pii, but otherwise I don't think we need this.


Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unpickled client missing data_collection

Low Severity

_Client.__setstate__ restores options from pickled state and calls _init_impl without ensuring a resolved data_collection entry exists. Clients pickled before this option was added have no data_collection key, so DSN-less Spotlight setup or the new data_collection property can raise KeyError.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 15c21c0. Configure here.

def is_active(self) -> bool:
"""
.. versionadded:: 2.0.0
Expand Down Expand Up @@ -614,6 +629,19 @@ def _record_lost_event(
self.options["error_sampler"] = sample_all
self.options["traces_sampler"] = sample_all
self.options["profiles_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,
include_local_variables=self.options["include_local_variables"]
is not False,
include_source_context=self.options["include_source_context"]
is not False,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spotlight frame flags disagree

Medium Severity

In DSN-less Spotlight mode, re-derived data_collection uses include_local_variables is not False (and the same for include_source_context), while _resolve_data_collection treats those options with bool(value) when they are not None. Falsy non-boolean values such as 0 resolve to disabled frame settings initially but become enabled after the Spotlight override.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 15c21c0. Configure here.

)
Comment thread
ericapisani marked this conversation as resolved.

self.session_flusher = SessionFlusher(capture_func=_capture_envelope)

Expand Down Expand Up @@ -724,6 +752,14 @@ def should_send_default_pii(self) -> bool:
"""
return self.options.get("send_default_pii") or False

@property
Comment thread
ericapisani marked this conversation as resolved.
Outdated
def data_collection(self) -> "DataCollection":
"""
Returns the resolved :class:`~sentry_sdk.data_collection.DataCollection`
config for this client.
"""
return self.options["data_collection"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to add this accessor?

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.

Probably not, but had added it to be consistent with dsn and should_send_default_pii. While I can kind of see why we have something for should_send_default_pii, I'm not sure why we have dsn in the first place - any context?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's also client options for which we do not provide accessors. We can always add the accessor later on if there's popular demand, but IMO it'd be good to avoid public API bloat.
If it's convenient for us in the integrations we can also keep a private _data_collection() accessor.

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.

Sounds good, will remove


@property
def dsn(self) -> "Optional[str]":
"""Returns the configured DSN as string."""
Expand Down
22 changes: 22 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class CompressionAlgo(Enum):
from sentry_sdk._types import (
BreadcrumbProcessor,
ContinuousProfilerMode,
DataCollectionUserOptions,
Event,
EventProcessor,
Hint,
Expand Down Expand Up @@ -1278,6 +1279,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,
http_proxy: "Optional[str]" = None,
https_proxy: "Optional[str]" = None,
ignore_errors: "Sequence[Union[type, str]]" = [], # noqa: B006
Expand Down Expand Up @@ -1432,6 +1434,26 @@ def __init__(
If you enable this option, be sure to manually remove what you don't want to send using our features for
managing `Sensitive Data <https://docs.sentry.io/data-management/sensitive-data/>`_.

.. deprecated::
Use `data_collection` instead. `send_default_pii` is still honored when `data_collection` is not set.

:param data_collection: Structured configuration controlling what data integrations collect automatically,
superseding `send_default_pii`. Pass a dict to enable or
restrict collection per category (user identity, cookies, HTTP headers/bodies, query params, generative AI
inputs/outputs, stack frame variables, source context).

When `data_collection` is set, 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 without configuring `data_collection` changes nothing. If both are set, `data_collection`
takes precedence.

Example::

sentry_sdk.init(
dsn="...",
data_collection={"user_info": False, "http_bodies": []},
)

:param event_scrubber: Scrubs the event payload for sensitive information such as cookies, sessions, and
passwords from a `denylist`.

Expand Down
Loading
Loading