Skip to content

_experiments={"data_collection": None} disables the event scrubber without enabling data collection #7346

Description

@JOhnsonKC201

data_collection is typed as Optional[DataCollectionUserOptions] in the Experiments TypedDict, so None type checks. But the two functions that read it disagree about what None means:

  • has_data_collection_enabled() (sentry_sdk/utils.py) checks for key presence: "data_collection" in options.get("_experiments", {})
  • _resolve_data_collection() (sentry_sdk/data_collection.py) only treats a non-None value as user provided: if user_dc is not None

So with data_collection=None the client lands in a state where neither side of the option is active. data_collection resolves to the legacy send_default_pii mapping with provided_by_user=False, but every caller of has_data_collection_enabled() takes the data collection branch.

Two consequences I was able to reproduce:

1. The default event scrubber is never installed.

In _get_options(), has_data_collection_enabled(rv) is true, so the EventScrubber is skipped. An explicitly configured event_scrubber is also discarded, with a warning saying data collection configuration was provided, which is not what happened.

sentry_sdk.init(_experiments={"data_collection": None})
sentry_sdk.get_isolation_scope().set_extra("password", "hunter2")
sentry_sdk.capture_message("hi")
config extra["password"] in the event
default (no _experiments) "[Filtered]"
data_collection={} "[Filtered]"
data_collection=None "hunter2"

Same for token and x_forwarded_for.

2. WSGI request attributes gain URL query params.

_get_request_attributes() takes the data collection branch and attaches http.query, url.path and url.full, using the denylist derived from send_default_pii=False. Without data_collection set at all, should_send_default_pii() is false and none of those are attached.

# QUERY_STRING = "email=a%40b.com&coupon=SAVE10", no data_collection set
attributes.get("http.query")  # None

# same request, _experiments={"data_collection": None}
attributes.get("http.query")  # 'email=a%40b.com&coupon=SAVE10'

Which behaviour is intended?

There is an existing test, test_has_data_collection_enabled_gates_on_presence, that asserts {"data_collection": None} returns True, so the presence check looks deliberate. But combined with _resolve_data_collection returning provided_by_user=False, the result is that a user who passes None loses default scrubbing and gains query param collection, which I doubt is intended either way.

Two ways to make the two functions agree:

  1. has_data_collection_enabled() gates on a configured value (... .get("data_collection") is not None), so None means "not configured". This matches the Optional annotation and _resolve_data_collection. It requires updating the existing test.
  2. _resolve_data_collection() treats a present-but-None value as {}, i.e. full opt in. That makes None mean "opt into the new defaults", which seems surprising for a value typed Optional.

I have a patch for option 1 with regression tests, happy to open a PR if that is the direction you want. Also happy to leave it if you would rather handle it as part of the wider send_default_pii migration.

Environment

sentry-sdk 2.68.1 (master at e0d105e), Python 3.13

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions