Files
hive-manage-v3/backend/.venv/lib/python3.12/site-packages/sentry_sdk/data_collection.py
T

262 lines
8.9 KiB
Python

"""
Data Collection configuration.
Implements the ``data_collection`` client option described in the Sentry SDK
"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
collected data by category (user identity, cookies, HTTP headers, 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``.
"""
import warnings
from typing import TYPE_CHECKING, cast
if TYPE_CHECKING:
from typing import Any, Dict, Literal
from sentry_sdk._types import (
DataCollection,
GenAICollectionBehaviour,
GraphQLCollectionBehaviour,
HttpHeadersCollectionBehaviour,
KeyValueCollectionBehaviour,
)
# ``http_bodies`` defaults to this (collect everything the
# platform supports); an empty list is the explicit opt-out.
# response bodyies are not included here because we don't
# currently capture them (as of Jul 7 2026)
_ALL_HTTP_BODY_TYPES = [
"incoming_request",
"outgoing_request",
]
# Default number of source lines captured above and below a stack frame.
_DEFAULT_FRAME_CONTEXT_LINES = 5
# Collection modes for key-value data (cookies, headers, query params).
# snake_case (Python-only deviation from the spec's camelCase); never
# serialized to Sentry.
_VALID_KEY_VALUE_COLLECTION_BEHAVIOUR_MODES = ("off", "denylist", "allowlist")
# Values of keys that contain any of
# these terms (partial, case-insensitive) are always replaced with
# ``"[Filtered]"`` regardless of the configured collection mode.
_SENSITIVE_DENYLIST = [
"auth",
"token",
"secret",
"password",
"passwd",
"pwd",
"key",
"jwt",
"bearer",
"sso",
"saml",
"csrf",
"xsrf",
"credentials",
"session",
"sid",
"identity",
]
def _map_from_send_default_pii(
*,
send_default_pii: bool,
include_local_variables: bool,
include_source_context: 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.
"""
kv_mode: "Literal['denylist', 'off']" = "denylist" if send_default_pii else "off"
terms = [] if send_default_pii else ["forwarded", "-ip", "remote-", "via", "-user"]
return {
"provided_by_user": False,
"user_info": send_default_pii,
"cookies": {"mode": kv_mode, "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),
"query_params": {"mode": kv_mode, "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": include_local_variables,
"frame_context_lines": (
_DEFAULT_FRAME_CONTEXT_LINES if include_source_context else 0
),
}
def _resolve_explicit(
d: "dict[str, Any]",
include_local_variables: bool,
include_source_context: bool,
) -> "DataCollection":
"""
Build a fully-resolved ``DataCollection`` from a user-supplied
``data_collection`` dict, filling in spec defaults for any omitted or
partially-specified field. Frame fields fall back to the legacy
``include_local_variables`` / ``include_source_context`` options when unset.
"""
# frame_context_lines accepts an integer or a boolean fallback (spec: True
# -> platform default of 5, False -> 0). bool is a subclass of int, so
# coerce explicitly before treating it as a line count.
frame_context_lines = d.get("frame_context_lines")
if frame_context_lines is None:
frame_context_lines = (
_DEFAULT_FRAME_CONTEXT_LINES if include_source_context else 0
)
elif isinstance(frame_context_lines, bool):
frame_context_lines = _DEFAULT_FRAME_CONTEXT_LINES if frame_context_lines else 0
stack_frame_variables = d.get("stack_frame_variables")
if stack_frame_variables is None:
stack_frame_variables = include_local_variables
# http_bodies: omitted means "all valid types"; [] is the explicit opt-out.
http_bodies = d.get("http_bodies")
http_bodies = (
list(http_bodies) if http_bodies is not None else list(_ALL_HTTP_BODY_TYPES)
)
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 {}),
"http_bodies": http_bodies,
"query_params": _kvcb_from_value(d.get("query_params") or {}),
"graphql": _graphql_from_value(d.get("graphql") or {}),
"gen_ai": _gen_ai_from_value(d.get("gen_ai") or {}),
"database_query_data": d.get("database_query_data", True),
"queues": d.get("queues", True),
"stack_frame_variables": stack_frame_variables,
"frame_context_lines": frame_context_lines,
}
def _kvcb_from_value(
val: "dict[str, Any]",
) -> "KeyValueCollectionBehaviour":
mode = val.get("mode", "denylist")
terms = val.get("terms", None)
if mode not in _VALID_KEY_VALUE_COLLECTION_BEHAVIOUR_MODES:
raise ValueError(
"Invalid collection mode {!r}. Must be one of {}.".format(
mode, _VALID_KEY_VALUE_COLLECTION_BEHAVIOUR_MODES
)
)
behaviour: "dict[str, Any]" = {"mode": mode}
if terms is not None:
behaviour["terms"] = list(terms)
return cast("KeyValueCollectionBehaviour", behaviour)
def _http_headers_from_value(
val: "dict[str, Any]",
) -> "HttpHeadersCollectionBehaviour":
return {
"request": (
_kvcb_from_value(val["request"])
if "request" in val
else _kvcb_from_value({"mode": "denylist"})
),
}
def _gen_ai_from_value(val: "dict[str, Any]") -> "GenAICollectionBehaviour":
return {
"inputs": val.get("inputs", True),
"outputs": val.get("outputs", True),
}
def _graphql_from_value(
val: "dict[str, Any]",
) -> "GraphQLCollectionBehaviour":
return {
"document": val.get("document", True),
"variables": val.get("variables", True),
}
def _resolve_data_collection(options: "Dict[str, Any]") -> "DataCollection":
"""
Resolve the effective ``DataCollection`` dict from client ``options``.
Reads ``data_collection``, ``send_default_pii``, ``include_local_variables``
and ``include_source_context`` and returns a fully-resolved dict with
concrete values for every field.
``data_collection`` must be a plain ``dict``.
"""
user_dc = options.get("_experiments", {}).get("data_collection")
send_default_pii = options.get("send_default_pii")
include_local_variables = (
bool(options.get("include_local_variables"))
if options.get("include_local_variables") is not None
else True
)
include_source_context = (
bool(options.get("include_source_context"))
if options.get("include_source_context") is not None
else True
)
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:
warnings.warn(
"`send_default_pii` is deprecated and ignored when "
"`data_collection` is set.",
DeprecationWarning,
stacklevel=2,
)
return _resolve_explicit(
user_dc,
include_local_variables,
include_source_context,
)
return _map_from_send_default_pii(
send_default_pii=bool(send_default_pii),
include_local_variables=include_local_variables,
include_source_context=include_source_context,
)