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

Metadata

Metadata

Assignees

No one assigned

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions