Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
35 changes: 28 additions & 7 deletions sentry_sdk/integrations/gcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import sentry_sdk
from sentry_sdk.api import continue_trace
from sentry_sdk.consts import OP
from sentry_sdk.data_collection import _apply_data_collection_filtering_to_query_string
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.integrations.cloud_resource_context import CLOUD_PROVIDER
Expand All @@ -20,6 +21,7 @@
TimeoutThread,
capture_internal_exceptions,
event_from_exception,
has_data_collection_enabled,
logger,
reraise,
)
Expand Down Expand Up @@ -100,10 +102,20 @@ def sentry_func(
if hasattr(gcp_event, "method"):
additional_attributes["http.request.method"] = gcp_event.method

if should_send_default_pii() and hasattr(gcp_event, "query_string"):
additional_attributes["url.query"] = gcp_event.query_string.decode(
"utf-8", errors="replace"
)
if hasattr(gcp_event, "query_string"):
query_string = gcp_event.query_string.decode("utf-8", errors="replace")
if query_string:
if has_data_collection_enabled(client.options):
filtered_qs = _apply_data_collection_filtering_to_query_string(
query_string=query_string,
behaviour=client.options["data_collection"][
"url_query_params"
],
)
if filtered_qs:
additional_attributes["url.query"] = filtered_qs
elif should_send_default_pii():
additional_attributes["url.query"] = query_string

sampling_context = {
"gcp_env": {
Expand Down Expand Up @@ -235,9 +247,18 @@ def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]":
request["method"] = gcp_event.method

if hasattr(gcp_event, "query_string"):
request["query_string"] = gcp_event.query_string.decode(
"utf-8", errors="replace"
)
query_string = gcp_event.query_string.decode("utf-8", errors="replace")
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if query_string:
filtered_qs = _apply_data_collection_filtering_to_query_string(
query_string=query_string,
behaviour=client_options["data_collection"]["url_query_params"],
)
if filtered_qs:
request["query_string"] = filtered_qs
else:
request["query_string"] = query_string

if hasattr(gcp_event, "headers"):
request["headers"] = _filter_headers(gcp_event.headers)
Expand Down
171 changes: 171 additions & 0 deletions tests/integrations/gcp/test_gcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,3 +786,174 @@ def cloud_function(functionhandler, event):
assert attrs["gcp.project.id"] == "serverless_project"
assert attrs["faas.identity"] == "func_ID"
assert attrs["faas.entry_point"] == "cloud_function"


# Sentinel: the query string (event) / ``url.query`` attribute (span) is absent.
NO_QUERY_STRING = "__NO_QUERY_STRING__"


# Each case is (send_default_pii, data_collection, expected_event, expected_span).
# ``send_default_pii`` / ``data_collection`` of None means the option is omitted.
# The two paths only diverge for the legacy (no ``data_collection``) rows: the
# event processor always records the raw query string, while the span-streaming
# path gates the ``url.query`` attribute on ``send_default_pii``.
_QUERY_STRING_DATA_COLLECTION_CASES = [
pytest.param(
True,
None,
"toy=tennisball&color=red&auth=secret",
"toy=tennisball&color=red&auth=secret",
id="send_default_pii_true",
),
pytest.param(
False,
None,
"toy=tennisball&color=red&auth=secret",
NO_QUERY_STRING,
id="send_default_pii_false",
),
pytest.param(
None,
None,
"toy=tennisball&color=red&auth=secret",
NO_QUERY_STRING,
id="defaults",
),
# data_collection configured: query string is routed through filtering.
# Spec defaults -> denylist: only the sensitive ``auth`` is redacted.
pytest.param(
None,
{},
"toy=tennisball&color=red&auth=[Filtered]",
"toy=tennisball&color=red&auth=[Filtered]",
id="data_collection_denylist_default",
),
pytest.param(
None,
{"url_query_params": {"mode": "denylist", "terms": ["toy"]}},
"toy=[Filtered]&color=red&auth=[Filtered]",
"toy=[Filtered]&color=red&auth=[Filtered]",
id="data_collection_denylist_custom_terms",
),
# allowlist with only ``toy`` allowed: ``color`` is redacted even though it
# is not sensitive, proving the redaction comes from the allowlist.
pytest.param(
None,
{"url_query_params": {"mode": "allowlist", "terms": ["toy"]}},
"toy=tennisball&color=[Filtered]&auth=[Filtered]",
"toy=tennisball&color=[Filtered]&auth=[Filtered]",
id="data_collection_allowlist",
),
pytest.param(
None,
{"url_query_params": {"mode": "off"}},
NO_QUERY_STRING,
NO_QUERY_STRING,
id="data_collection_off",
),
]


def _build_init_kwargs(send_default_pii, data_collection):
"""Render the keyword-argument string passed to ``init_sdk`` in the
subprocess."""
kwargs = []
if send_default_pii is not None:
kwargs.append("send_default_pii=%r" % send_default_pii)
if data_collection is not None:
kwargs.append("_experiments=%r" % {"data_collection": data_collection})

return ", ".join(kwargs)


@pytest.mark.parametrize(
"send_default_pii, data_collection, expected_event, expected_span",
_QUERY_STRING_DATA_COLLECTION_CASES,
)
def test_query_string_data_collection_event_processor(
run_cloud_function,
send_default_pii,
data_collection,
expected_event,
expected_span,
):
init_kwargs = _build_init_kwargs(send_default_pii, data_collection)
envelope_items, _, _ = run_cloud_function(
dedent(
"""
functionhandler = None

from collections import namedtuple
GCPEvent = namedtuple("GCPEvent", ["headers", "method", "query_string"])
event = GCPEvent(
headers={},
method="GET",
query_string=b"toy=tennisball&color=red&auth=secret",
)

def cloud_function(functionhandler, event):
raise Exception("something went wrong")
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(%s)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
% init_kwargs
)
)

request = envelope_items[0]["request"]
if expected_event == NO_QUERY_STRING:
assert "query_string" not in request
else:
assert request["query_string"] == expected_event


@pytest.mark.parametrize(
"send_default_pii, data_collection, expected_event, expected_span",
_QUERY_STRING_DATA_COLLECTION_CASES,
)
def test_query_string_data_collection_span_streaming(
run_cloud_function,
send_default_pii,
data_collection,
expected_event,
expected_span,
):
init_kwargs = _build_init_kwargs(send_default_pii, data_collection)
_, _, span_items = run_cloud_function(
dedent(
"""
functionhandler = None

from collections import namedtuple
GCPEvent = namedtuple("GCPEvent", ["headers", "method", "query_string"])
event = GCPEvent(
headers={},
method="GET",
query_string=b"toy=tennisball&color=red&auth=secret",
)

def cloud_function(functionhandler, event):
return "ok"
"""
)
+ FUNCTIONS_PRELUDE
+ dedent(
"""
init_sdk(traces_sample_rate=1.0, trace_lifecycle="stream", %s)
gcp_functions.worker_v1.FunctionHandler.invoke_user_function(functionhandler, event)
"""
% init_kwargs
)
)

assert len(span_items) == 1
attrs = span_items[0]["attributes"]
if expected_span == NO_QUERY_STRING:
assert "url.query" not in attrs
else:
assert attrs["url.query"] == expected_span
Loading