Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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)

Comment thread
ericapisani marked this conversation as resolved.
## 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"]'
Comment thread
ericapisani marked this conversation as resolved.
Outdated
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):

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.

For my understanding, we have these type pairs (UserOptions and Behaviour) so that users don't need to specify all options (total=False on the UserOptions types), but also so that the materialized DataCollection that we'll use internally won't nag us about an option potentially being None, because it has to be fully filled out with default values in case any are missing, right?

I see why we have it like this but I'm afraid the two might drift eventually because we forget to update one of them.

It's not like I have better ideas 😀 The complexity we introduce with the config needs to be dealt with someplace, and I guess this is an ok place.

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.

For my understanding, we have these type pairs (UserOptions and Behaviour) so that users don't need to specify all options (total=False on the UserOptions types), but also so that the materialized DataCollection that we'll use internally won't nag us about an option potentially being None, because it has to be fully filled out with default values in case any are missing, right?

Yup, you've got it! I wanted to spare us from all the extra code we'd have to write to satisfy the typechecker when performing the checks within the integrations 😅

I agree with you about the risk of drift. I thought it'd be worth it in this case because after the data collection spec is implemented and settled, the amount of updates we'd need to do here would likely be minimal.

If we think of anything better throughout the implementation of the spec though, happy to come back and change the approach! 😄

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
Comment thread
ericapisani marked this conversation as resolved.
Outdated

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"]
Comment thread
ericapisani marked this conversation as resolved.
Outdated

@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